var
is not block scoped, meaning that var
in a for loop will leak into the parent scope, which is not something we necessarily want. Typically we want a placeholder value that we need to work with only inside the loop.
for (var i = 0; i < 3; i++){
console.log(i);
}
// 0
// 1
// 2
console.log(i); // 3
The above is equivalent to this:
var i;
for (i = 0; i < 3; i++){
console.log(i);
}
// 0
// 1
// 2
console.log(i); // 3
That was a pretty benign example. Let's now take a look at the more consequential examples. Here, we are going to increment a counter and print each number with a staggered timeout delay.
for (var i = 0; i < 3; i++){
setTimeout(function(){
console.log(i)
}, 1000 * i)
}
// 3
// 3
// 3
Not what you might expect, right? The catch is that i
is bound to whatever i
is at the time of execution. Because of the setTimeout
, by the time the function executes, i
has already been mutated at every iteration.
One quick way to fix this is by using let
. What do we know about let? It’s block-scoped. As a note, you couldn’t use a const for this because the variable needs to overwrite itself as we iterate through. When we use let, it’s going to scope i
to our curly brackets.
for (let i = 0; i < 3; i++){
setTimeout(function(){
console.log(i);
}, 1000 * i)
}
// 0
// 1
// 2
If you're having a hard time understanding why let
works like this, check out this equivalent representation:
for (var i = 0; i < 3; i++) {
let j = i;
setTimeout(function(){
console.log(j); // j remembers what it was at each iteration
}, 1000 * i);
}
Before ES6, we had no let
and const
, which meant that developers had to use a clever trick using a closure.
var funcs = [];
for (var i = 0; i < 3; i++) {
var returnFunctionWithBoundInteger = function(i){
return function(){
setTimeout(function(){
console.log(i);
}, i * 1000);
}
}
funcs[i] = returnFunctionWithBoundInteger(i);
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
This enclosing function (aptly named returnFunctionWithBoundInteger
) is important because at each iteration, we want to lock in an i
value for the returned function. Remember: functions follow lexical scope; they remember variables in their original referencing environment even when invoked outside of their original position in the code. Therefore, at each iteration, we are storing an inner function that remembers the i
it had access to at each iteration. If you need a refresher on closures, check out this resource.
Fun fact: debugging a loop leak was one of the first interview questions I bombed. I changed the var
to let
, and the interviewer asked me to keep var
and find another solution. I couldn't figure it out, even though I vaguely new about closures. For loop leak is actually a pretty common interview question, so I hope you don't make the same mistake I did.
Warmly,
EK
Top comments (0)