DEV Community

Cover image for JS / explain for loop leak
icncsx
icncsx

Posted on • Edited on

JS / explain for loop leak

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
Enter fullscreen mode Exit fullscreen mode

The above is equivalent to this:

var i;
for (i = 0; i < 3; i++){
    console.log(i);
}
// 0
// 1
// 2

console.log(i); // 3
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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]();
}
Enter fullscreen mode Exit fullscreen mode

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)