56

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.

Explosion Pills
  • 188,624
  • 52
  • 326
  • 405
  • 2
    You've pretty much said it in your question, the `let` is essentially re-evaluated each time the loop iterates. I don't know that I'd call it syntactic sugar, it's just how loops are defined to work. – loganfsmyth Jun 17 '15 at 18:40
  • possible duplicate of [Javascript - "let" keyword vs "var" keyword](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword) –  Jun 17 '15 at 19:11
  • See also http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword. –  Jun 17 '15 at 19:11
  • Read this 2 posts: [LET in ECMAScript 6 - Block Scoped Variables](http://blogjs.github.io/ecmascript6/2016/06/08/es6-let-block-scoped-variables/) , [Constants in ECMAScript 6](http://blogjs.github.io/ecmascript6/2016/06/06/es6-constants/) – Anton Temchenko Jun 13 '16 at 17:49
  • Am I correct that in here: let i = 100; setTimeout(function () { console.log(i) }, i); // '100' after 100 ms we can also use var i = 100? – Marek Nov 19 '17 at 01:02

6 Answers6

70

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++;
          …
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    This is probably more correct and certainly more concise than my answer. – ssube Jun 17 '15 at 19:16
  • 1
    @Bergi loved your answer, but I still, I cannot really visualize how the execution context would look like. If you had the time could you please edit your answer to show how this would look like? – Kostas Dimakis Mar 01 '17 at 11:39
  • @KonstantinosDimakis which context do you mean? – Bergi Mar 01 '17 at 14:49
  • @Bergi the one with the `let` – Kostas Dimakis Mar 01 '17 at 14:54
  • It's just a scope that contains one variable `i`. It is referenced by the closure, and it references the scope in which the loop was contained in as its outer link. – Bergi Mar 01 '17 at 15:04
  • @Bergi I still have several questions: 1, what do ```_status``` and ```{i}``` mean in ```__status = {i};```? 2, In each loop, there is a new variable called ```i```? I cannot understand. At beginning, I thought in each loop, ```i``` is updated and passed into the function ```process.nextTick()```. In the function ```process.nextTick```, a new variable is generated and have the same value with ```i```? Am I wrong? Thanks – BAE Apr 08 '17 at 15:28
  • @BAE 1) `__status` is part of the loop context state, in my syntax denoted by an object, that is used to transport the current value(s) of the block-declared variable(s) in the loop header from one scope to the next. 2) Yes, each iteration creates a new scope with a new variable `i`. This does not depend on the statements in the block (like the `process.nextTick` call) though – Bergi Apr 08 '17 at 18:08
  • I had to create a new topic to help me understand this problem. For anyone that still does not understand it, try here https://stackoverflow.com/questions/47372465/loops-and-closures-for-and-var – Marek Nov 19 '17 at 01:10
  • To be completely exact: there is one scope missing in your explanation. The for-statement (the bracketed part of it) should be nested in its own block scope: `if (i < 10) { process.nextTick(_ => console.log(i)) } __status = ...`. It is possible to shadow the counter `i` in this scope by declaring its own `i` (`let i = 'something completely different') without breaking the flow. – Min-Soo Pipefeet Mar 21 '19 at 13:00
  • @Min-SooPipefeet You mean each body of the loop should have two nested scopes? – Bergi Mar 21 '19 at 13:05
  • 1
    @Bergi I'm not sure if I understand your question correctly. I mean that your explanation does not reflect the fact that `for (let i = 0; i < 10; ++i) { let i = 'a string'; process.nextTick(_ => console.log(i)) }` would not break the flow, producing 10x `a string`. To reflect this fact, you need to nest `let i = 'a string'; process.nextTick(_ => console.log(i));` in an own scope by enclosing it with brackets. – Min-Soo Pipefeet Mar 21 '19 at 13:12
  • @Min-SooPipefeet I checked the spec, you're right. If the body of the loop is a block (it doesn't always need to be, but it needs to be if it contains a `let` declaration), then that block of course introduces its own block scope inside the block scope caused by the `let` in the loop head. I've updated my answer to just omit the block in the example :-) – Bergi Mar 21 '19 at 13:27
  • @Bergi Yes, from the syntactical point of view, there were a (pretty useless) possibility to use a declaration in the loop without a block: `let i = ( /* do stuff with a comma-separated line of expressions */, 'string');` But the interpreter refuses to accept this: `SyntaxError: Lexical declaration cannot appear in a single-statement context`. :-) – Min-Soo Pipefeet Mar 21 '19 at 14:50
  • @Min-SooPipefeet Exactly, from the syntactical point of view the grammar does not allow this :-) If they would have allowed it, I guess `for (let i=…;;) let i=…;` would have caused an "idenfier already declared" error. – Bergi Mar 21 '19 at 14:56
  • cf _"copy the values from all variables with those names from one to the next environment"_ If I declare a function or block-scoped variable in the block body of a for loop, I presume a corresponding function-object is instantiated on a per iteration basis when the LE for the iteration is created. So in this case a function or variable is declared with the same identifier across iterations, but the value is _not_ copied between iterations. If this is correct, then the quoted part of the answer might need clarification that it only applies to the contents of the initializer expression(?) – Ben Aston Feb 26 '20 at 01:09
  • @Ben That's exactly what "*those names*" refers to – Bergi Feb 26 '20 at 02:09
  • In my comment I think I am making a distinction between the `perIterationBindings` and bindings in the body of the for loop itself. – Ben Aston Feb 26 '20 at 14:22
  • @Ben I made the same distinction. I was never talking about the bindings declared in the body of the loop. – Bergi Feb 26 '20 at 14:31
23

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.

Neithan Max
  • 11,004
  • 5
  • 40
  • 58
swapnil_mishra
  • 501
  • 5
  • 7
9

let introduces block scoping and equivalent binding, much like functions create a scope with closure. I believe the relevant section of the spec is 13.2.1, where the note mentions that let declarations are part of a LexicalBinding and both live within a Lexical Environment. Section 13.2.2 states that var declarations are attached to a VariableEnvironment, rather than a LexicalBinding.

The MDN explanation supports this as well, stating that:

It works by binding zero or more variables in the lexical scope of a single block of code

suggesting that the variables are bound to the block, which varies each iteration requiring a new LexicalBinding (I believe, not 100% on that point), rather than the surrounding Lexical Environment or VariableEnvironment which would be constant for the duration of the call.

In short, when using let, the closure is at the loop body and the variable is different each time, so it must be captured again. When using var, the variable is at the surrounding function, so there is no requirement to reclose and the same reference is passed to each iteration.

Adapting your example to run in the browser:

// prints '10' 10 times
for (var i = 0; i < 10; i++) {
  setTimeout(_ => console.log('var', i), 0);
}

// prints '0' through '9'
for (let i = 0; i < 10; i++) {
  setTimeout(_ => console.log('let', i), 0);
}

certainly shows the latter printing each value. If you look at how Babel transpiles this, it produces:

for (var i = 0; i < 10; i++) {
  setTimeout(function(_) {
    return console.log(i);
  }, 0);
}

var _loop = function(_i) {
  setTimeout(function(_) {
    return console.log(_i);
  }, 0);
};

// prints '0' through '9'
for (var _i = 0; _i < 10; _i++) {
  _loop(_i);
}

Assuming that Babel is fairly conformant, that matches up with my interpretation of the spec.

ssube
  • 47,010
  • 7
  • 103
  • 140
  • @TinyGiant Most browsers can't run the first snippet, as they tend not to support let and arrow functions. – ssube Jun 17 '15 at 19:03
  • 1
    I had to read the spec several times -- I still don't think I fully understand but looking at Babel's output helps. Essentially using `let` does create a new scope in each iteration. I suppose this is just built into the language. Babel also doesn't like mixing the declarations `let i = 0, var j = 1` which would be incompatible anyway. – Explosion Pills Jun 17 '15 at 19:06
  • @ExplosionPills The spec around scopes(/environments/bindings) tends to get a little obtuse. I still have a hard time understanding it, but it seems like the effective result is that each iteration has its own binding. Babel/ES6 doesn't let you mix `let a = 1, const b = 2` either: you just can't mix different declaration types in a statement. – ssube Jun 17 '15 at 19:09
0

Recently I got confused about this problem too. According to the above answers, here is my understanding:

for (let i=0;i<n;i++)
{
   //loop code
}

is equivalent to

// initial
{
    let i=0
}
// loop
{
    // Sugar: For-Let help you to redefine i for binding it into current block scope
    let i=__i_value_from_last_loop__

    if (i<=n){
        //loop code
    }
    i++
}
jack
  • 21
  • 2
0

Let us see “let” and “var” with the setTimeout majorly asked in the interview.

(function timer() { 
   for (var i=0; i<=2; i++) 
       { setTimeout(function clog() {console.log(i)}, i*1000); } 
    })();

(function timer() { 
   for (let i=0; i<=2; i++) 
       { setTimeout(function clog() {console.log(i)}, i*1000); } 
    })();

Let's see in detail how this code executes in the javascript compiler. The answer for “var” is “222” due to functional scope and for “let” is “012” because it is block scope.

Now let us look at what it looks like in detail when it Compiles for "var". (It's a little hard to explain over the code than in audio or video but I am trying my best to give you.)

var i = 0;

if(i <=2){
setTimeout(() => console.log(i));
}
i++;  // here the value of "i" will be 1

if(i <=2){
setTimeout(() => console.log(i));
}
i++;   // here the value of "i" will be 2

if(i <=2){
setTimeout(() => console.log(i));
}
i++;  // here the value of "i" will be 3

After the code is executed finally it will print all the console.log where the value of "i" is 6. So the final output is: 222

In "let i" will be declared in every scope. The import point to be noted here is "i" will get the value from the previous scope and not from the declaration. (Below code is just an example of how it looks like in the compiler and trying it wont work)

{
    //Scope  1
    { 
    let i;  
    i= 0;
    
    
    if(i<=2) {
        setTimeout(function clog() {console.log(i)};);
    }
    i++;   // Here "i" will be increated to 1
    
    }
    
    //Scope 2  
    // Second Interation run
    {
    let i;
    i=0;
    
        // Even “i” is declared here i= 0 but it will take the value from the previous scope
    // Here "i" take the value from the previous scope as 1
    if(i<=2) {    
        setTimeout(function clog() {console.log(i)}; );
    }
    
    i++;   // Here “i” will be increased to 2
    
    }
    
    
    //Scope 3 
    // Second Interation run
    {
    let i;
    i=0;
    
    // Here "i" take the value from the previous scope as 2
    if(i<=2) {   
        setTimeout(function clog() {console.log(i)}; );
    }
    
    i++;   // Here "i" will be increated to 3
    
    }
    

}

So, it will print "012" value as per the block scope.

Muthukumar
  • 554
  • 4
  • 9
0

Let is a block scope. Var declared inside for loop can be accessed even outside the for loop because var is only function scope. You cant access var defined inside of a function from outside. With each iteration new Let gets created. But since var is function scope and it is available ouside the for loop and kind of becomes global and with every iteration the same var variable gets updated.

raj
  • 21
  • 3
  • Welcome to Stack Overflow! Make sure to always include code on your answer! [How do I write a good answer?](https://stackoverflow.com/help/how-to-answer) – William Brochensque junior Mar 03 '23 at 18:19
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 03 '23 at 18:19