63

ECMAScript 6's let is supposed to provide block scope without hoisting headaches. Can some explain why in the code below i in the function resolves to the last value from the loop (just like with var) instead of the value from the current iteration?

"use strict";
var things = {};
for (let i = 0; i < 3; i++) {
    things["fun" + i] = function() {
        console.log(i);
    };
}

things["fun0"](); // prints 3
things["fun1"](); // prints 3
things["fun2"](); // prints 3

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. Why is that necessary?

"use strict";
var things = {};
for (let i = 0; i < 3; i++) {
    let index = i;
    things["fun" + i] = function() {
        console.log(index);
    };
}

things["fun0"](); // prints 0
things["fun1"](); // prints 1
things["fun2"](); // prints 2

I tested the script with Traceur and node --harmony.

Krzysztof Szafranek
  • 758
  • 1
  • 6
  • 10
  • 1
    TL;DR: The the first code snippet demonstrates a bug in the JavaScript implementation of the environment. That is, the correct behavior is to output 0, 1 and 2, instead of 3, 3 and 3. Modern browsers work correctly and output 0, 1 and 2 when you run the code. – Utku Jul 15 '17 at 12:25
  • Apparently there are three different scopes here: `let x = 5; for (let x = 0; x < 10; x++) { let x = 3; console.log(x); }` – joeytwiddle Aug 02 '17 at 13:19
  • See also [Explanation of `let` and block scoping with for loops](https://stackoverflow.com/q/30899612/1048572) – Bergi Oct 02 '21 at 15:06

3 Answers3

74

squint's answer is no longer up-to-date. In ECMA 6 specification, the specified behaviour is that in

for(let i;;){}

i gets a new binding for every iteration of the loop.

This means that every closure captures a different i instance. So the result of 012 is the correct result as of now. When you run this in Chrome v47+, you get the correct result. When you run it in IE11 and Edge, currently the incorrect result (333) seems to be produced.

More information regarding this bug/feature can be found in the links in this page;

Since when the let expression is used, every iteration creates a new lexical scope chained up to the previous scope. This has performance implications for using the let expression, which is reported here.

Ryan B
  • 3,364
  • 21
  • 35
neuron
  • 1,896
  • 1
  • 19
  • 24
  • 15
    Oddly though, while it is a separate instance if `i` in each iteration of the loop, any modification of `i` within the loop still affects the loop's iteration count. It's an odd beast, half separate variable, half reference to the original. – jfriend00 Mar 08 '16 at 01:07
  • 3
    @jfriend00 at the beginning of each new iteration, a new lexical scope is established, and the value of "i" in previous scope is copied over. – neuron Mar 11 '16 at 03:58
  • 6
    Yes, I know that. But, it isn't a pure copy because modifying it during the loop still somehow affects the loop variable. So, it must be something like at the end of each loop execution it's value is copied back to the loop counter. – jfriend00 Mar 11 '16 at 04:24
  • 1
    @jfriend00 well observed. – Giorgi Moniava Mar 04 '17 at 17:07
  • 3
    @jfriend00 The way I understand it is this: At the beginning of the code block, the value of `i` is copied into a new local variable that's what you reference with the identifier `i`. At the end of that block, it's copied back into the loop variable, which in the next iteration is copied back down, etc. (There's a more efficient way to implement it, but that was the simplest way for me to think about it) – Nic Nov 17 '17 at 07:01
  • @QPaysTaxes - Not sure why you're responding to a comment from a year and a half ago, but yes that's pretty much how to think about it. – jfriend00 Nov 17 '17 at 08:14
  • 5
    @jfriend00 (a) Retroactively, because it might help someone else reading this, and (b) because I didn't notice the date when I wrote the comment. – Nic Nov 17 '17 at 08:15
  • Thank you jfriend00 -- I was wondering a) whether everyone else was nuts in thinking that was a new i, or b) how weird it was to create a new i every time. Then you point out this.. feature. The JS people are NUTS. – Gerard ONeill Mar 07 '18 at 21:46
22

I passed this code through Babel so we can understand the behaviour in terms of familiar ES5:

for (let i = 0; i < 3; i++) {
    i++;
    things["fun" + i] = function() {
        console.log(i);
    };
    i--;
}

Here is the code transpiled to ES5:

var _loop = function _loop(_i) {
    _i++;
    things["fun" + _i] = function () {
        console.log(_i);
    };
    _i--;
    i = _i;
};

for (var i = 0; i < 3; i++) {
    _loop(i);
}

We can see that two variables are used.

  • In the outer scope i is the variable that changes as we iterate.

  • In the inner scope _i is a unique variable for each iteration. There will eventually be three separate instances of _i.

    Each callback function can see its corresponding _i, and could even manipulate it if it wanted to, independently of the _is in other scopes.

    (You can confirm that there are three different _is by doing console.log(i++) inside the callback. Changing _i in an earlier callback does not affect the output from later callbacks.)

At the end of each iteration, the value of _i is copied into i. Therefore changing the unique inner variable during the iteration will affect the outer iterated variable.

It is good to see that ES6 has continued the long-standing tradition of WTFJS.

joeytwiddle
  • 29,306
  • 13
  • 121
  • 110
  • 3
    +1 for including the Babel translation which is really useful to see, thanks! However, just because Babel has to use wtf hacks to translate the let keyword to work in older non-ES6 compliant browsers certainly doesn't mean that ES6 is wack in modern browser implementations. – charlie roberts Nov 09 '17 at 03:39
  • 1
    Agreeing with Charlie. But in addition, the WTFJS stands. Why would you want to assume that the loop counter is a different variable every time through the loop? I would want to assume it is the same (LET in this case would indicate local to the for loop). I consider this a brain fart of the largest magnitude. The code to make i be the same (but different than the outer scope) is much harder now than adding a single line to create a new variable to use. – Gerard ONeill Mar 07 '18 at 21:53
  • This also doesn't represent that in ES6+, `for (let i = 0, ....)` creates a locally scoped `i` that is not visible outside the `for` loop. The transpiled version does a `var i` which would be function scoped. That does not seem fully representative of how ES6+ works. – jfriend00 Nov 09 '19 at 20:11
  • To demonstrate that, readers can open the Babel link and add `const i = 5` to the top of the code. Babel will then [output](https://babeljs.io/repl/#?babili=false&browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=MYewdgzgLgBAljAvDArAbgFAYGYgE4wAUANgKawLIAMa8MAPDAMy1wDUbAlDAN4YwD4HTIJhQAFnDABzCAG0ARNgCuYBTDbwAukhgqwwKHHCFufUaNCQQZAHTEQ0wnE4jBAXzcC4AWh-Z3IA&sourceType=module&lineWrap=false&presets=es2015%2Creact%2Cstage-2&version=7.7.3) three vars: `i`, `_i` and `_i2` – joeytwiddle Nov 10 '19 at 05:52
4

IMHO -- the programmers who first implemented this LET (producing your initial version's results) did it correctly with respect to sanity; they may not have glanced at the spec during that implementation.

It makes more sense that a single variable is being used, but scoped to the for loop. Especially since one should feel free to change that variable depending on conditions within the loop.

But wait -- you can change the loop variable. WTFJS!! However, if you attempt to change it in your inner scope, it won't work now because it is a new variable.

I don't like what I have to do To get what I want (a single variable that is local to the for):

{
    let x = 0;
    for (; x < length; x++)
    {
        things["fun" + x] = function() {
            console.log(x);
        };
    }
}

Where as to modify the more intuitive (if imaginary) version to handle a new variable per iteration:

for (let x = 0; x < length; x++)
{
    let y = x;
    things["fun" + y] = function() {
        console.log(y);
    };
}

It is crystal clear what my intention with the y variable is.. Or would have been if SANITY ruled the universe.

So your first example now works in FF; it produces the 0, 1, 2. You get to call the issue fixed. I call the issue WTFJS.

ps. My reference to WTFJS is from JoeyTwiddle above; It sounds like a meme I should have known before today, but today was a great time to learn it.

Gerard ONeill
  • 3,914
  • 39
  • 25