4 ms·
Right, I forgot that it was triggered by var vs let. > so each iteration has a completely different binding. That was what confused me about this rule as well
by xg15 5y ago
Right, I forgot that it was triggered by var vs let.
> so each iteration has a completely different binding.
That was what confused me about this rule as well: If you write "for (let i = 0; i<n; i++)", this will actually create n+1 independent variables, all named "i" but bound to different scopes.
The variables can even be independently modified:
You can write
for (var i=0; i<3; i++)
setInterval(() => {console.log(i); i+=100}, 1000);
This will print the "expected" output:
3
103
203
303
403
...
i.e. all the closures bound to the same variable. The same output is produced by
let i; for (i=0; i<3; i++)
setInterval(() => {console.log(i); i+=100}, 1000);
However, if you write
for (let i=0; i<3; i++)
setInterval(() => {console.log(i); i+=100}, 1000);
you get:
0
1
2
100
101
102
200
201
202
...
i.e. each closure logs - and increments(!) - its own copy of i.
Neither "for" nor "let" on their own have this behaviour, it's a special language rule which seems to be triggered by using "let" in the first clause of a "for" statement.
I can understand the intention behind the design, but I feel that, in order to make a common usecase less surprising, they made the whole thing actually more surprising.
- masklinn 5y ago> I can understand the intention behind the design, but I feel that, in order to make a common usecase less surprising, they made the whole thing actually more surprising. I don't agree with that, `for let` simply behaves as if the `let` was inside the loop: for (var _i=0; _i<3; _i++) { let i = _i; setInterval(() => {console.log(i); i+=100}, 1000); } there you go, same behaviour as a for-let.
- chrismorgan 5y agoI agree with xg15 that it’s surprising, however well-intentioned. The general wisdom is that C-style for loops are simple sugar for while loops, that the following two are equivalent: for (initialiser; condition; incrementer) { body; } initialiser; while (condition) { body; incrementer; } This made perfect sense and was easy to reason about. Add lexical scoping, and it should obviously have just gained an extra level of scope, so that bindings are limited to the loop: { initialiser; while (condition) { body; incrementer; } } But instead, the initialiser became awkwardly half inside the loop and half outside the loop: inside as regards lexical bindings, but outside as regards execution (… and lexical bindings in the remainder of the initialiser). That’s wacky. I understand why they did it, and in practice it was probably the right decision, but it’s logically pretty crazy, and much more convoluted for the spec and implementation. https://262.ecma-international.org/6.0/#sec-for-statement-runtime-semantics-labelledevaluation https://262.ecma-international.org/6.0/#sec-for-statement-ru... (warning: 4MB of slow HTML) and the following headings show how complicated it makes it. In essence, you end up with this: { initialiser; { Lexically rebind every name declared in the initialiser. (Roughly `let i = i;`, if only that shadowed a parent, as it does in many such languages, rather than raising a ReferenceError.) while (condition) { body; incrementer; } } } This reveals also problems in your attempted desugaring: for (var _i=0; _i<3; _i++) { let i = _i; setInterval(() => {console.log(i); i+=100}, 1000); } The trouble is that the condition and incrementer are not referring to a var, but rather to your lexical declaration that you put on the next line.
- xg15 5y agoThought so as well first. But then again, you can write for (let i=0; i<10; i++) { console.log(i); i++; } And it correctly logs "1 3 5 7 9". So if anything, the desugaring would have to be something like for (var _i=0; _i<3; _i++) { let i = _i; setInterval(() => {console.log(i); i+=100}, 1000); _i = i; } Haven't tried out putting closures inside the condition or incrementer to see if things get even weirder.