I know this has already been answered several times but I’m hoping this explanation will help somebody.
Personally I am also not a fan of “that’s just how it works” explanations. I always want to know the origin and the reasoning for why something works the way it does.
Let’s look at this code in detail
for (var i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i * 1000 );
}
The reason why this behaves the way it does lies in the nature of the setTimeout
function.
The function is known as an asynchronous function. This means that whenever the interpreter sees a call to such a function it doesn’t run it immediately but puts it on a “back burner” for later. The “later” happens when all the regular (synchronous) code has been executed.
After all regular code has been executed, the interpreter goes back and runs all the asynchronous functions that have been put on the queue.
In this case the for loop is synchronous code.
This is what an interpreter actually sees
for (var i=1; i<=5; i++) {
//put something on the todo list for later
}
The interpreter runs this loop in a fraction of a second and puts 5 things on the “to-do” one after another. It only takes a very small amount of time to register the function on the queue. At this point no inner code has run.
When the interpreter finally gets around to running the 5 actions that have been put on the “later” queue, the for loop is long since finished running, and the variable i
has been set to 6. This is because the last action that for loop executed is i++
making i
equal 6, right before the condition check i <= 5 failed (because i
is no longer less then or equal to 5)
In short by looking at this for loop we can see that the necessary condition for it to terminate would be met when i
becomes 6
Finally the interpreter gets to running those 5 actions that were put on the queue earlier which means it will attempt to run console.log(i)
5 times.
The problem is that it uses i
which is long since been set to 6, the actions that were waiting in queue had no way of retaining the value of i
that was passed in when they were registered. All they know is to look for i whatever it is now. And now it is equal to 6.
The easiest way to solve this problem is to wrap everything inside the loop in a IIFE. This is an Immediately Invoked Function Expression. In a nutshell it’s simply an anonymous function which is immediately called
(function(){
})();
On the surface it might look like this has no purpose, but the reason this is important is because in javascript a function retains its context. When you call setTimeout()
and set the timer()
function to execute, the i
variable does not exist inside timer()
so it uses the i
from the outer context. In this case the i
that ends up being used is the i
that has long since been set to 6.
Now let’s write the same code with an IIFE
for (var i=1; i<=5; i++) {
(function(i){
setTimeout( function timer(){
console.log( i );
}, i * 1000 );
})(i);
}
This code will run exactly as expected.
What is happening here is that as the for loop runs 5 times (very fast) it calls the anonymous function 5 times (each time with a different i). Then the anonymous function proceeds to set the timeout. Effectively this creates a new i
for each call, because javascript uses function scoping. i
inside the IIFE is not the same i
that the for loop is incrementing. This time when the interpreter finally gets around to running the timer function, the i
that i will use is the i
that was passed to the IIFE, and not the global i
that was modified by the for loop.
I hope i have made that more clear for you.
If you have any questions let me know.