Loops + Closure
The most common canonical example used to illustrate closure involves the humble for-loop.
for (var i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
Note: Linters often complain when you put functions inside of loops, because the mistakes of not understanding closure are so common among developers. We explain how to do so properly here, leveraging the full power of closure. But that subtlety is often lost on linters and they will complain regardless, assuming you don’t actually know what you’re doing.
The spirit of this code snippet is that we would normally expect for the behavior to be that the numbers “1”, “2”, .. “5” would be printed out, one at a time, one per second, respectively.
In fact, if you run this code, you get “6” printed out 5 times, at the one-second intervals.
Huh?
Firstly, let’s explain where 6
comes from. The terminating condition of the loop is when i
is not <=5
. The first time that’s the case is when i
is 6. So, the output is reflecting the final value of the i
after the loop terminates.
This actually seems obvious on second glance. The timeout function callbacks are all running well after the completion of the loop. In fact, as timers go, even if it was setTimeout(.., 0)
on each iteration, all those function callbacks would still run strictly after the completion of the loop, and thus print 6
each time.
But there’s a deeper question at play here. What’s missing from our code to actually have it behave as we semantically have implied?
What’s missing is that we are trying to imply that each iteration of the loop “captures” its own copy of i
, at the time of the iteration. But, the way scope works, all 5 of those functions, though they are defined separately in each loop iteration, all are closed over the same shared global scope, which has, in fact, only one i
in it.
Put that way, of course all functions share a reference to the same i
. Something about the loop structure tends to confuse us into thinking there’s something else more sophisticated at work. There is not. There’s no difference than if each of the 5 timeout callbacks were just declared one right after the other, with no loop at all.
OK, so, back to our burning question. What’s missing? We need more cowbell closured scope. Specifically, we need a new closured scope for each iteration of the loop.
We learned in Chapter 3 that the IIFE creates scope by declaring a function and immediately executing it.
Let’s try:
for (var i=1; i<=5; i++) {
(function(){
setTimeout( function timer(){
console.log( i );
}, i*1000 );
})();
}
Does that work? Try it. Again, I’ll wait.
I’ll end the suspense for you. Nope. But why? We now obviously have more lexical scope. Each timeout function callback is indeed closing over its own per-iteration scope created respectively by each IIFE.
It’s not enough to have a scope to close over if that scope is empty. Look closely. Our IIFE is just an empty do-nothing scope. It needs something in it to be useful to us.
It needs its own variable, with a copy of the i
value at each iteration.
for (var i=1; i<=5; i++) {
(function(){
var j = i;
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})();
}
Eureka! It works!
A slight variation some prefer is:
for (var i=1; i<=5; i++) {
(function(j){
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})( i );
}
Of course, since these IIFEs are just functions, we can pass in i
, and we can call it j
if we prefer, or we can even call it i
again. Either way, the code works now.
The use of an IIFE inside each iteration created a new scope for each iteration, which gave our timeout function callbacks the opportunity to close over a new scope for each iteration, one which had a variable with the right per-iteration value in it for us to access.
Problem solved!
Block Scoping Revisited
Look carefully at our analysis of the previous solution. We used an IIFE to create new scope per-iteration. In other words, we actually needed a per-iteration block scope. Chapter 3 showed us the let
declaration, which hijacks a block and declares a variable right there in the block.
It essentially turns a block into a scope that we can close over. So, the following awesome code “just works”:
for (var i=1; i<=5; i++) {
let j = i; // yay, block-scope for closure!
setTimeout( function timer(){
console.log( j );
}, j*1000 );
}
But, that’s not all! (in my best Bob Barker voice). There’s a special behavior defined for let
declarations used in the head of a for-loop. This behavior says that the variable will be declared not just once for the loop, but each iteration. And, it will, helpfully, be initialized at each subsequent iteration with the value from the end of the previous iteration.
for (let i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
How cool is that? Block scoping and closure working hand-in-hand, solving all the world’s problems. I don’t know about you, but that makes me a happy JavaScripter.