I understand that let
prevents duplicate declarations which is nice.
let x;
let x; // error!
Variables declared with let
can also be used in closures which can be expected
let i = 100;
setTimeout(function () { console.log(i) }, i); // '100' after 100 ms
What I have a bit of difficulty grasping is how let
applies to loops. This seems to be specific to for
loops. Consider the classic problem:
// prints '10' 10 times
for (var i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }
// prints '0' through '9'
for (let i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }
Why does using let
in this context work? In my imagination even though only one block is visible, for
actually creates a separate block for each iteration and the let
declaration is done inside of that block ... but there is only one let
declaration to initialize the value. Is this just syntactic sugar for ES6? How is this working?
I understand the differences between var
and let
and have illustrated them above. I'm particularly interested in understanding why the different declarations result in different output using a for
loop.
According to MDN using let in the for loop like that should bind the variable in the scope of the loop's body. Things work as I'd expect them when I use a temporary variable inside the block.
Block scoped variables: A block scoped variable means that the variable defined within a block will not be accessible from outside the block. A block can reside inside a function, and a block scoped variable will not be available outside the block even if the block is inside a function.
You can see the same behavior while using let if you've already declared the variable before using it in your for loop. Now it would behave the same way as the var for loop did. var is function scoped (in layman terms globally scoped) i.e. value declared using var is available throughout the program.
The main difference is the scope difference, while let can be only available inside the scope it's declared, like in for loop, var can be accessed outside the loop for example.
Is this just syntactic sugar for ES6?
No, it's more than syntactic sugar. The gory details are buried in §13.6.3.9
CreatePerIterationEnvironment
.
How is this working?
If you use that let
keyword in the for
statement, it will check what names it does bind and then
Your loop statement for (var i = 0; i < 10; i++) process.nextTick(_ => console.log(i));
desugars to a simple
// omitting braces when they don't introduce a block
var i;
i = 0;
if (i < 10)
process.nextTick(_ => console.log(i))
i++;
if (i < 10)
process.nextTick(_ => console.log(i))
i++;
…
while for (let i = 0; i < 10; i++) process.nextTick(_ => console.log(i));
does "desugar" to the much more complicated
// using braces to explicitly denote block scopes,
// using indentation for control flow
{ let i;
i = 0;
__status = {i};
}
{ let {i} = __status;
if (i < 10)
process.nextTick(_ => console.log(i))
__status = {i};
} { let {i} = __status;
i++;
if (i < 10)
process.nextTick(_ => console.log(i))
__status = {i};
} { let {i} = __status;
i++;
…
I found this explanation from Exploring ES6 book the best:
var-declaring a variable in the head of a for loop creates a single binding (storage space) for that variable:
const arr = []; for (var i=0; i < 3; i++) { arr.push(() => i); } arr.map(x => x()); // [3,3,3]
Every i in the bodies of the three arrow functions refers to the same binding, which is why they all return the same value.
If you let-declare a variable, a new binding is created for each loop iteration:
const arr = []; for (let i=0; i < 3; i++) { arr.push(() => i); } arr.map(x => x()); // [0,1,2]
This time, each i refers to the binding of one specific iteration and preserves the value that was current at that time. Therefore, each arrow function returns a different value.
let
introduces block scoping and equivalent binding, much like functions create a scope with closure. I believe the relevant section of the spec is 13.2.1, where the note mentions that let
declarations are part of a LexicalBinding and both live within a Lexical Environment. Section 13.2.2 states that var
declarations are attached to a VariableEnvironment, rather than a LexicalBinding.
The MDN explanation supports this as well, stating that:
It works by binding zero or more variables in the lexical scope of a single block of code
suggesting that the variables are bound to the block, which varies each iteration requiring a new LexicalBinding (I believe, not 100% on that point), rather than the surrounding Lexical Environment or VariableEnvironment which would be constant for the duration of the call.
In short, when using let
, the closure is at the loop body and the variable is different each time, so it must be captured again. When using var
, the variable is at the surrounding function, so there is no requirement to reclose and the same reference is passed to each iteration.
Adapting your example to run in the browser:
// prints '10' 10 times
for (var i = 0; i < 10; i++) {
setTimeout(_ => console.log('var', i), 0);
}
// prints '0' through '9'
for (let i = 0; i < 10; i++) {
setTimeout(_ => console.log('let', i), 0);
}
certainly shows the latter printing each value. If you look at how Babel transpiles this, it produces:
for (var i = 0; i < 10; i++) {
setTimeout(function(_) {
return console.log(i);
}, 0);
}
var _loop = function(_i) {
setTimeout(function(_) {
return console.log(_i);
}, 0);
};
// prints '0' through '9'
for (var _i = 0; _i < 10; _i++) {
_loop(_i);
}
Assuming that Babel is fairly conformant, that matches up with my interpretation of the spec.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With