let keyword in the for loop

JavascriptEcmascript 6

Javascript Problem Overview


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.

Javascript Solutions


Solution 1 - Javascript

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.

Solution 2 - Javascript

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.

Solution 3 - Javascript

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKrzysztof SzafranekView Question on Stackoverflow
Solution 1 - JavascriptneuronView Answer on Stackoverflow
Solution 2 - JavascriptjoeytwiddleView Answer on Stackoverflow
Solution 3 - JavascriptGerard ONeillView Answer on Stackoverflow