It's not just that `let` is block-scoped, it's that a for-let loop scopes the variable inside the loop iteration, so each iteration has a completely different binding.
You can have block scoping and your loops be scoped the other way around still:
r := []func() int{}
for i := range [5]int{} {
r = append(r, func() int { return i })
}
fmt.Println(r[0]()) // 4
r = r[:0]
for i := 0; i < 5; i++ {
r = append(r, func() int { return i })
}
fmt.Println(r[0]()) // 5
You can even have different loop types be scoped differently e.g. in C#, a C-style for loop will update the binding in-place but a foreach loop will not:
var r = new List<Func<int>>();
for(int i=0; i<5; ++i) {
r.Add(() => i);
}
Console.WriteLine(r[0]()); // 5
r.Clear();
foreach(int i in Enumerable.Range(0, 5)) {
r.Add(() => i);
}
Console.WriteLine(r[0]()); // 0
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.
> 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);
}
I 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-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.
You can have block scoping and your loops be scoped the other way around still:
You can even have different loop types be scoped differently e.g. in C#, a C-style for loop will update the binding in-place but a foreach loop will not: