This question is a few different (I think related) questions, which I give below, but broadly I am trying to understand a code piece from David Flanagan's O'Reilly book on JavaScript that gives an example as using a loop for creating closures as unintended:
function constfuncs() {
let funcs = [];
for(var i = 0; i < 10; i++) {
funcs[i] = () => i;
}
return funcs;
}
let funcs = constfuncs();
funcs[5]() //10, rather than 5
The above code example intends to have each element of the funcs
array be a function that can be called without arguments to return a value numerically equivalent to its index in funcs
; however, it does not succeed at doing this. I think I largely understand why this is: var i
, because it is declared using var
, is scoped to the function constfuncs
, rather than the block of that for
loop, and so hence it is the same i
being used by each function stored in funcs
, and i
ends as 10
in that loop, so hence any of the ten returned functions in funcs
will always evaluate to 10
when called.
On the next page of the book, it's stated that to fix the above, it's a one-line change: simply have i
be block-scoped, and so use let i
instead of var i
in that for
loop, as follows:
function constfuncs() {
let funcs = [];
for(let i = 0; i < 10; i++) {
funcs[i] = () => i;
}
return funcs;
}
let funcs = constfuncs();
funcs[5]() //is now 5, as desired
What I don't understand, however, is how this fixes the problem. i
to me seems to still exist only once, yet each of the ten created functions appears to have access to its own version/copy of i
in memory. If a new block-scoped local variable was declared in the for
loop that was assigned the value of i
on each iteration prior to then being used instead of i
as the value each function of funcs
returned, then perhaps it would make more sense to me, if it is the case that block-scoped local variables declared in a loop are redeclared (as using new memory) on each iteration; but even so, i
is not and yet the above still works as desired. This leads me to my questions:
let
and const
are block scoped, each iteration of the loop defines a scope that is independent of the scopes for all other iterations, and each of these scopes has its own independent binding of i
."Questions 1-2 are related, as are 3-4; and all are related to the above example. Any other thoughts for explaining why the second approach above (with let i
) succeeds in fixing the solution are welcome.
Technically I'm not answering any of you questions, but I can explain the part where you wondered "how this fixes the problem":
Variables declared with let
in the for-loop initialization are treated as if they were declared inside the loops "body", by definition. That is a special behavior of for-loops with let
.
Variables declared with var
on the other hand have the same scope as where the for-loop is defined (i.e. outside of the loops "body").
So if you would declare the let i
outside the for-loop, you would get the wrong value 10
again (as you expected):
function constfuncs(){
let funcs = [];
let i;
for( i = 0; i < 10; i++ ){
funcs[ i ] = function(){
return i;
};
}
return funcs;
}
On the other hand, even if you keep the var
inside the loop initialization, but declare a new variable with let
inside the loops body, you would get the desired value 5
:
function constfuncs(){
let funcs = [];
for( var i = 0; i < 10; i++ ){
let value = i; // <-- new variable in every iteration
funcs[ i ] = () => value;
}
return funcs;
}
The newer let
declaration is specifically designed for this behavior, because people tended to be confused about the behavior of var
.
(I case you wonder: IMO let
has too many special cases. let
makes the behavior more intuitive when you don't think about it, but more complicated if you want to understand it. But it still solves important flaws of var
.)