Explosion Pills
Explosion Pills

Reputation: 191749

Explanation of `let` and block scoping with for loops

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.

Upvotes: 58

Views: 9838

Answers (6)

raj
raj

Reputation: 41

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.

Upvotes: 0

Muthukumar
Muthukumar

Reputation: 654

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.

Upvotes: 0

jack
jack

Reputation: 21

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++
}

Upvotes: 0

Bergi
Bergi

Reputation: 664548

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++;
          …

Upvotes: 73

swapnil_mishra
swapnil_mishra

Reputation: 511

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.

Upvotes: 24

ssube
ssube

Reputation: 48267

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.

Upvotes: 9

Related Questions