Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does 'let' in for loop do this? [duplicate]

I understand that let prevents duplicate declarations which is nice.

let x;
let x; // error!

Variables declared with let can also be used in closures which can be expected

let i = 100;
setTimeout(function () { console.log(i) }, i); // '100' after 100 ms

What I have a bit of difficulty grasping is how let applies to loops. This seems to be specific to for loops. Consider the classic problem:

// prints '10' 10 times
for (var i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }
// prints '0' through '9'
for (let i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }

Why does using let in this context work? In my imagination even though only one block is visible, for actually creates a separate block for each iteration and the let declaration is done inside of that block ... but there is only one let declaration to initialize the value. Is this just syntactic sugar for ES6? How is this working?

I understand the differences between var and let and have illustrated them above. I'm particularly interested in understanding why the different declarations result in different output using a for loop.

like image 549
Explosion Pills Avatar asked Sep 15 '26 11:09

Explosion Pills


2 Answers

Is this just syntactic sugar for ES6?

No, it's more than syntactic sugar. The gory details are buried in §13.6.3.9 CreatePerIterationEnvironment.

How is this working?

If you use that let keyword in the for statement, it will check what names it does bind and then

  • create a new lexical environment with those names for a) the initialiser expression b) each iteration (previosly to evaluating the increment expression)
  • copy the values from all variables with those names from one to the next environment

Your loop statement for (var i = 0; i < 10; i++) process.nextTick(_ => console.log(i)); desugars to a simple

// omitting braces when they don't introduce a block
var i;
i = 0;
if (i < 10)
    process.nextTick(_ => console.log(i))
    i++;
    if (i < 10)
        process.nextTick(_ => console.log(i))
        i++;
        …

while for (let i = 0; i < 10; i++) process.nextTick(_ => console.log(i)); does "desugar" to the much more complicated

// using braces to explicitly denote block scopes,
// using indentation for control flow
{ let i;
  i = 0;
  __status = {i};
}
{ let {i} = __status;
  if (i < 10)
      process.nextTick(_ => console.log(i))
      __status = {i};
}   { let {i} = __status;
      i++;
      if (i < 10)
          process.nextTick(_ => console.log(i))
          __status = {i};
    }   { let {i} = __status;
          i++;
          …
like image 71
Bergi Avatar answered Sep 17 '26 01:09

Bergi


I found this explanation from Exploring ES6 book the best:

var-declaring a variable in the head of a for loop creates a single binding (storage space) for that variable:

const arr = [];
for (var i=0; i < 3; i++) {
    arr.push(() => i);
}
arr.map(x => x()); // [3,3,3]

Every i in the bodies of the three arrow functions refers to the same binding, which is why they all return the same value.

If you let-declare a variable, a new binding is created for each loop iteration:

const arr = [];
for (let i=0; i < 3; i++) {
    arr.push(() => i);
}

arr.map(x => x()); // [0,1,2]

This time, each i refers to the binding of one specific iteration and preserves the value that was current at that time. Therefore, each arrow function returns a different value.

like image 45
swapnil_mishra Avatar answered Sep 17 '26 01:09

swapnil_mishra