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