Some confusion on the let keyword and for loops

Recently I read on hacks.mozilla.org that:

Loops of the form for (let x…) create a fresh binding for x in each iteration.

But today I came across a bug in my project that suggested this is wrong.
So I tried some code and this is the result:

for( let i of "abc" ) {
  setTimeout( () => console.log(i), 1000 );
}
// Output: c, c, c

for( let i in "abc" ) {
  setTimeout( () => console.log(i), 1000 );
}
// Output: 2, 2, 2

let sting = "abc";
for( let i = 0; i < sting.length; i++ ) {
  setTimeout( () => console.log(i), 1000 );
}
// Output: 0, 1, 2

Only the last loop acts as the blog stats, even though the post specifically says that all for loops should have their lets scoped per iteration.
And MDN doesn’t specify how this should work.

So does anyone know what is right and what is not?
You would at least expect all three to act the same.

BTW, when you declare a let in the body of the loops, they are scoped per iteration.

1 Like

Man let is something that trips me up from time to time. I focus on using var though because let hasn’t got to places outside of Firefox yet and I’ve been doing some code here and there for non-Fx projects.

But man super post. I’m not sure exactly whats right or not, but this was an awesome post by @nmaier on teaching me let. It’s similar to the mozhacks article but it might help, it helped me with hoisting: http://codereview.stackexchange.com/a/56895/49106

The first section titled “Regarding var vs. let”

Loops of the form for (let x…) create a fresh binding for x in each iteration.

It refers to the value kept in the memory.
Look at this example:

let arr = 'this';

for (let i in arr) {
   console.log(i); // logs 0, 1, 2, 3
}

for (let i of arr) {
   console.log(i); // logs t, h, i, s
}

Above works as one expects.

Now look at this:

let arr = 'this';

for (let i in arr) {
   console.log(i); // logs 0, 1, 2, 3
   window.setTimeout(function() { console.log(i) }, 1000); // logs 3, 3, 3, 3
}

What happens is this:
A value for i is allocated while inside the for loop block (N.B. inside the block there is ONLY one i)

  1. Iteration 1: i is set to 0, 1st function logs 0, 2nd function waits
    for 1 second (i is 0)
  2. Iteration 2: i is set to 1, 1st function logs 1, 2nd function waits
    for 1 second (i is 1), since the timeout for 1st iteration hasn’t
    been reached the i in the 1st iteration timeout is also set to 1
  3. Iteration 3: i is set to 2, 1st function logs 2, 2nd function waits
    for 1 second (i is 2), since the timeout for 1st & 2nd iteration
    haven’t been reached the i in the 1st & 2nd iteration timeouts are also set
    to 2
  4. Iteration 4: i is set to 3, 1st function logs 3, 2nd function waits
    for 1 second (i is 3), since the timeout for 1st, 2nd & 3rd
    iteration haven’t been reached the i in the 1st, 2nd & 3rd iteration timeouts
    are also set to 3
  5. timeout 1: logs 3
  6. timeout 2: logs 3
  7. timeout 3: logs 3
  8. timeout 4: logs 3

There can be a situation when the timeout catches up with the non-timedout function (ie very short timeout or complex non-timedout function, which can result in even more unpredictable outcome.

To simplify, timeout inside a loop is not a good idea and should be avoided.

The same applies to var for the same reason. (N.B. inside the block there is ONLY one i)

let arr = 'this';

for (var i in arr) {
   console.log(i); // logs 0, 1, 2, 3
   window.setTimeout(function() { console.log(i) }, 1000); // logs 3, 3, 3, 3
}

The is no difference between var & let inside a block. The difference between them is only outside a block where variables set with let inside the block, are no longer available.

Just for fun …

let arr = 'this';

for (let i in arr) {
   console.log(i); // logs 0, 1, 2, 3
   delay(i);
}

function delay(i) { // this i changes with every iteration
  
  window.setTimeout(function() { console.log(i) }, 1000); // logs 0, 1, 2, 3
}
1 Like

The author of the blog posses this problem as an example:

var messages = ["Meow!", "I'm a talking cat!", "Callbacks are fun!"];
for (var i = 0; i < messages.length; i++) {
  setTimeout(function () {
    cat.say(messages[i]);
  }, i * 1500);
}

And states that this can be fixed by replacing the var with let, which is correct.
But he also states that this works the same the for...in and for...of loops, which is incorrect.

Language specifications are far too technical for me.
But I doubt they would make a description of let where the behavior differs across for loops.

Furthermore I just did a test on Chrome, and there let does behave as per the blog post.

I think I’ll see if I can contact the author, he doesn’t happen to have an account here, does he?

BTW, all the examples you posted(specifically the last one), work exactly the same var or let.

As already stated.

for (let a in 'this') {
   // some code
}
console.log(a); 
// ReferenceError: a is not defined, a is only available inside above block

But with var …

for (var b in 'this') {
   // some code
}
console.log(b); 
// logs 3, b, which was last set as 3, is available outside above block

Ah, I thought the last part was to show an actually difference, my bad.

This is a difference though:

for (let a in 'this') {
  var b = a;
  setTimeout( () => console.log( b ), 1000 );
}
// Output: 3, 3, 3, 3

for (let a in 'this') {
  let b = a;
  setTimeout( () => console.log( b ), 1000 );
}
// Output: 0, 1, 2, 3

Coudn’t find a way to contact the author btw.

As I said, "The is no difference between var & let inside a block."
I shall clarify by saying it means a single block and not nested blocks.

What you need to consider is that in your example there are 2 blocks (not one)
a in for loop is inside the entire for block.
b is inside another block with extends to a single iteration.

In the first example, the var b extends to both blocks and it gets reassigned with each iteration so by the time it want to output the timeout function, it has been reassigned to the last value.

In the second example, the let b extends to inner block only an it gets destroyed with each iteration so by the time it want to output the timeout function, 4 separate values have been created.

The difference is the value passed from one block to the other since a & b are in different blocks.

Yeah, I think a better way to say it is, var is hoisted to the function scope.

Just to be clear.
I understand the how and why.
I just don’t understand why their is a difference among for loops.
Which makes me doubt Firefox’s implementation.

The first two loops are logging a property, or the value of a property, of a constant string. Note that they are not logging i, and so the scope of i does not affect the value getting logged. The variable i has block scope, but the constant string does not change during or after the loop.

The third loop is logging the value of i, and the result depends whether i is declared by var or let. Personally, I wouldn’t write any code that relies on this behaviour, it is far too subtle and easy to get wrong. What you see in the third loop is almost an accidental consequence of the fact that i is not hoisted and so a closure is created for it each time. Better to make it an explicit closure.

All very well knowing what is happening, but I got to thinking maybe what you describe is the way it should work. Turns out there is an existing bug for this, and even hints that it may be fixed soon:
https://bugzilla.mozilla.org/show_bug.cgi?id=449811

The regular for/let loop binding was only fixed (updated really, since the new behaviour was only defined in ES6) in FF39:
https://bugzilla.mozilla.org/show_bug.cgi?id=854037

1 Like

Ah, cool man!
That was what I was looking for all along.

And that is actually the author of the original blog post working on the bug, haha :smile: