What do you expect the code below to return?

  • 0, 1, 2
  • 3, 3, 3
  • 3
  • undefined
let i; for (i = 0; i < 3; i++) { const log = () => { console.log(i); }; setTimeout(log, 1000); }

It's 3, 3, 3 :<

It's all to do with scope, closures. This happens because the i variable is outside of the lexical scope of the for loop. Loops don't have scope, only functions do (unless using const or var introduced in ES2015, but only if they are declared inside a block {}).

So when setTimeout tries to access i when log is called, it actually gets the i which is outside of the block, the let i; which we've declared in the global scope, and which has then been incremented to 3, all in that one line, immediately.

I say it in my head like this: "i equals zero, and then as long is i is less than 3, increment i -> i returns 3" It loops, and assigns the value of the incremented i, to the global scope, before the loop runs for the closure function which accesses the inner i, which has confusingly picked up the "global" scoped value of 3.

So setTimeout just grabs the sum i value, which is 3. Because the loop still runs 3 times, we get 3, 3, 3.

let i; console.log(i); for (i = 0; i < 3; i++) { const log = () => { console.log(i); }; setTimeout(log, 1000); }

The setTimeout has nothing to do with it really it's a red herring. Think of it as any function calling log(), which creates a closure.

Could be absolutely anything, and if you set the timer of setTimeout() to 0, it still grabs the i which is outside of the for block's scope.

It's the same as having i declared with var, which doesn't have lexical scope in a for loop (unless through closure in a function).

If you want to make this work you can declare i with let and initialise it with the null value 0 as a for loop expression at the start. Let is lexically scoped as opposed to var, which is always global (unless within function scope), and being as we're declaring it within the for block initialiser, it will increment as we would like it to, creating a new instance of i every time it runs and then incrementing.

for (let i = 0; i < 3; i++) { const log = () => { return i); }; setTimeout(log, 100); } // => 0, 1, 2

If we were to do this the ES5 way without leveraging the lexical scope powers of let and const it would look like this:

var i; for (i = 0; i < 3; i++) { var log = () => { console.log(i); }; setTimeout( (function(inner_i) { return function() { console.log(inner_i); }; })(i), 1000 ); }

I pass in a local version of i called innner_i as it is scoped to the inner function, it "closes over" i So here I take advantage of the closures inner scope, and we get 0, 1, 2 as we want.

Or you create an anonymous IIFE (iffy) which is an Immediately Invoked Function Expression like this which also creates a closure.

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

This has most commonly tripped me up when iterating over HTML collections in JS DOM manipulations when using el.getElementsByTagName() or el.querySelectorAll() so, watch out for this in those cases :D