6

This uses var

var a = [];
for (var i = 0; i < 10; i++) {
    a[i] = function() {
        console.log(i);
    };
}
a[6](); // 10

This uses let

var a = [];
for (let i = 0; i < 10; i++) {
    a[i] = function() {
        console.log(i);
    };
}
a[6](); // 6

I don't understand why the result is different. Can somebody guide me?

Salman A
  • 262,204
  • 82
  • 430
  • 521
hui
  • 591
  • 7
  • 22

4 Answers4

5

The resulting array consists of functions, each function body looks like this:

console.log(i);

The value of i depends on whether we used var or let to declare the variable.

var (ECMAScript 5 and 6)

Here i is a global variable whose value is 10 after exiting the loop. This is the value that is logged.

let (ECMAScript 6)

Here i is a local variable whose scope is restricted to the for statement. Moreover, this variable gets a fresh binding on each iteration. This is best explained by your code transpiled to ECMAScript 5:

"use strict";
var a = [];
var _loop = function(i) {
    a[i] = function() {
        console.log(i);
    };
};
for (var i = 0; i < 10; i++) {
    _loop(i);
}
a[6](); // 6

So, on seventh iteration for example, the value of i will be 6 (counting from zero). The function created inside the iteration will refer to this value.

Community
  • 1
  • 1
Salman A
  • 262,204
  • 82
  • 430
  • 521
2

I think it would be much better to not define functions in a loop, you could easily accomplish this with one function definition that returns a closure:

function logNumber(num) {
   return function() {
     console.log(num);
   }
}

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = logNumber(i);
}
a[6]();

Regarding the difference between the two examples, one is using let for block scoping. A better example that shows the difference would be:

ECMA5:

for (var i = 0; i < 10; i++) { }
console.log(i); // 10

ECMA6:

for (let i = 0; i < 10; i++) { }
console.log(i); // i is not defined

Edit: as I stated in my comment to your question, this is more likely a side-effect of the transpiler you are using. Firefox supports block scoping and both versions of your loop produce 10 as output (which they should).

Rob M.
  • 35,491
  • 6
  • 51
  • 50
  • for ecma6 you should write like so: – hui May 21 '15 at 06:46
  • @dukegod I don't understand what you mean – Rob M. May 21 '15 at 06:49
  • All well and good, but this does not answer the question of why the OP sees two different results. –  May 21 '15 at 06:50
  • @torazaburo If you jump over to a browser that supports `let` (like Firefox) instead of using a transpiler, both versions produce 10. I believe the oddness of OP's result has more to do with transpiling than the behavior of `let` in the ECMA6 spec. – Rob M. May 21 '15 at 06:53
  • Yes, I understand how it's supposed to work. But the OP's question was not "why is it 10", which is a perfectly reasonable but separate question, but rather it was, "why is the behavior different". I think the correct answer to the question as stated is "The behavior is different because you are using a transpiler which is buggy". –  May 21 '15 at 06:55
2

This is correct behavior according to the spec. The behavior with var and let is defined to be different.

See the spec, at https://people.mozilla.org/~jorendorff/es6-draft.html#sec-forbodyevaluation. According to this, the relevant concepts, which make the function declared inside the loop close over the current value of the block-scoped loop index, are things called "per-iteration bindings" and "per-iteration environment".

Babel handles it correctly, producing the following code:

var a = [];
var _loop = function (i) {
    a[i] = function () {
        console.log(i);
    };
};

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

This implements the semantics of for (let by isolating the contents of the for loop into a separate function parameterized by the index. By virtue of doing that, the function no longer closes over the for loop index, and i is treated separately in each function created. Thus the answer is 6.

Traceur does not produce the correct result. It yields 10.

So the famous question that has been asked 100 times on SO, about why my function declared in a loop and closing over the index index is using the "wrong" value of the loop index, shall be asked no more?

The issue is a bit more nuanced that merely proclaiming that "of course, let is block-scoped". We know that. We get how it works in an if block, for example. But what's going on here is a bit of an twist on block scoping in the context of a for, hitherto unknown to many people including me. It's a variable actually declared outside the "block" (if you think of the block as the body of the for statement) but has a separate existence inside each iteration of the loop.

For more, see https://github.com/babel/babel/issues/1078.

  • [Babel's output is `6`](http://babeljs.io/repl/#?experimental=true&evaluate=true&loose=false&spec=false&code=var%20a%20%3D%20%5B%5D%3B%0A%20for%20(let%20i%20%3D%200%3B%20i%20%3C%2010%3B%20i%2B%2B)%20%7B%0A%20%20%20%20a%5Bi%5D%20%3D%20function%20()%20%7B%0A%20%20%20%20%20console.log(i)%3B%0A%20%20%20%20%20%7D%3B%0A%20%20%20%7D%0A%20a%5B6%5D()%3B%20%2F%2F%206). Not sure how you arrive at 10? – Felix Kling May 21 '15 at 13:18
  • @FelixKling Sorry, brain aneurysm, swapped output from Babel and Traceur. Fixed. –  May 21 '15 at 13:21
  • Ah. Well, if Traceur really produces 10 for the second example, its incorrect. `let` is block scoped. – Felix Kling May 21 '15 at 13:22
1

Why is result different in ES6 and ES5?

Because let and var are different. let is block-scoped while var is function-scoped.

In your first example there is only a single variable i. Every function you create has a reference to the same variable i. At the moment you call a[6](), i has the value 10, because that was the termination condition for the loop.

In the second example, every iteration of the loop has it's own variable i. It works exactly like in other languages with block scope.

Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143