Variable scope of JavaScript was weird. JavaScript used to have only dynamic
scope with var
, which makes all variables in a function have the same function
scope regardless of presence of block or name shadowing. It was why ES2015
introduced new lexical declarations, let
and const
.
let i = 10;
{
let i = 20;
console.log(i); // => 20
}
console.log(i); // => 10
It seems like working with for
loop as expected.
let i = 10;
for (let i = 0; i < 5; i++) {
console.log(i); // => 0, 1, 2, 3, 4
}
console.log(i); // => 10
But yesterday one thing confused me. Please guess the result of the following code.
let fs = [];
for (let i = 0; i < 5; i++) {
fs.push(() => i);
}
console.log(fs.map(f => f()));
My guess was [5,5,5,5,5]
, as I thought the i
is shared through the for
loop. A long version of my guess is like below:
- A lexical variable
i
is created onfor (let i = 0;
- A closure returning the
i
is pushed tofs
i
is increased by 1- A closure returning the
i
is pushed tofs
i
is increased by 1- ... iterates while
i < 5
... - When the loop finishes,
i
is not GCed as there are closures referencing thei
- And
i
is now 5 - All the closures return
i
, which is 5 - The result is
[5,5,5,5,5]
And basically, I was wrong. The result is [0,1,2,3,4]
, which means i
in the
block is actually a new i
in every iteration:
- A lexical variable
i
is created onfor (let i = 0;
- A closure returning the variable is pushed to
fs
- A new
i
is bound with the value of the oldi
i
is increased by 1- A closure returning the variable is pushed to
fs
- A new
i
is bound with the value of the oldi
i
is increased by 1- ... iterates ...
- All the
i
s returned by the closures are actually all different bindings - The result is
[0,1,2,3,4]
In short, when used with lexical declaration, for
loop binds a new variable
for each iteration, while a previous value is actually inherited. It is
specified in Standard ECMA-262.
I am not sure other languages with lexical declaration and C-styled for
loop
do the same, but at least C++ doesn't do this and I was a bit confused. I
understand that capturing a variable with closure is very common in JS and
sharing the variable through the closures is error-prone, but I still think that
the case should be handled by programmers, not language spec.