DEV Community

loading...

Closures/Scope and the setTimeout for loop question

levimeahan profile image Levi Meahan ・2 min read

I've researched the event loop in JavaScript before, and have a decent general understanding of how it works, but I was recently caught off guard by this classic interview question.

// What does the below code output?

for(var i = 0; i < 4; i++) {
   setTimeout(function() {
      console.log(i);
   }, i * 1000);
}

// Answer: 4 4 4 4

I've seen this around and remembered that it's a trick question, and there are some ways to solve it like using let i = 0; instead of var i = 0; but I didn't fundamentally understand why that worked. (If you want a great, very detailed explanation of this question, check out this post: https://medium.freecodecamp.org/thrown-for-a-loop-understanding-for-loops-and-timeouts-in-javascript-558d8255d8a4)

But there was still one thing I didn't get. The callback gets put into the event queue, so certainly it has to keep a copy of i in order to use it, right? Since our main code is going to finish running before the callback gets called, shouldn't the variable i not exist anymore? I didn't see an explanation, so I went to go review MDN's article on Closures.

The article explains - Closures (aka the callback we pass to setTimeout) keep a reference to the environment/scope they were created in, including references to its variables, even after that environment/scope stops running. Ohhhh. Suddenly this makes a lot more sense. Even after our main code finishes, a reference to its variables(at a minimum, the ones the closure uses) is kept around for the closure to access. So if the i used in our callback is a global variable within that environment, the closure will use that reference.

Thus, the many solutions to this question revolve around creating a different scope for i to exist in each time we call setTimeout, so that each callback in the event queue maintains a reference to a completely different variable, and we really have 4 different variables all named i, in different scopes.

Which actually raises an interesting, not completely obvious behavior of the following solution:

for(let i = 0; i < 4; i++) {
   setTimeout(function() {
      console.log(i);
   }, i * 1000);
}

// 0 1 2 3

For this to work, we know that i needs to actually be a different variable each time we call setTimeout. Which means that when we use let in a for loop like this, it's actually creating a new variable named i on every iteration of the loop. Never really thought about it like that!

And this also raises a somewhat important bit of performance knowledge to keep in mind - if we maintain a reference to a closure, we're maintaining a reference to the entire scope it was defined in (even with possible compiler optimization, it's at least the variables used within the closure).

Further reading that explains much more about closures and scope:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let

https://github.com/getify/You-Dont-Know-JS/tree/master/scope%20%26%20closures

Discussion (0)

pic
Editor guide