Var, and let in a loop working differently

I’m running a loop to print the same variable using var and let.

for (var val = 0; val < 10; val++) {
    console.log(val);
    setTimeout(function() {
        console.log(`The number is ${val}`);
    }, 1000);
}

This above block of code log 0-9 correctly but then prints “The number is 10” ten times instead of printing “The number is 0”, “The number is 1” and so on.

However, if I change the variable declaration to ‘let’. Then it prints out correctly.

for (let i = 0; i < 10; i++) {
    console.log(i);
    setTimeout(function() {
        console.log(`The number is ${i}`);
    }, 1000);
}

This above block of code prints the statements correctly. Each statement is printed after a delay of one second like “The number is 0”, “The number is 1” and so on.

I’m not able to understand what is going on here. Can someone break it down for me?

1 Like

This is because of hoisting. Declaring a variable with var in your for loop doesn’t limit its scope to just that block of code like it would using let. Since its existence extends beyond that for loop, when the value is evaluated by the setTimeout() callback, you get the last value it was set to. (it gets set to 10, which is why the for loop stops)

for (var val = 0; val < 10; val++) {
  console.log(val);
  setTimeout(function() {
    console.log(`The number is ${val}`);
  }, 1000);
}
if (val === 10) {
  console.log('I can still see the value');
}

You can see the same behavior while using let if you’ve already declared the variable before using it in your for loop.

let i;
for (i = 0; i < 10; i++) {
  console.log(i);
  setTimeout(function() {
    console.log(`The number is ${i}`);
  }, 1000);
}

Now it would behave the same way as the var for loop did.

1 Like

var is function scoped (in layman terms globally scoped) i.e. value declared using var is available throughout the program. Whereas, let is locally scoped i.e. it’s only available to a particular block. You need to make the val available locally using let within the loop for you to see the change as pointed out by @selectall .

You can also try this code of snippet to understand the difference. Difference between var and let

Thank you for the explanation @selectall and Thank you for the linking the resource to explain the difference between var and let, @pulsating_photon. However, I should mention that I do understand that let is block scoped and var is only function scoped and that they can’t be accessed outside these defined scopes. I also understand the idea that var sort of leaks out to the global scope but I’m unable to see what the association is between the two(scope and this problem) here.

What I’m more interested to understand is why do these loops behave in this way where one prints ‘The number is 10’ ten times and other prints ‘The number is 0’, ‘The number is 1’ and so on after the delay of one second. How is the function accessing the already changed values because the function is not printing it right after and the loop would continue to increment the value? What I’m failing to understand is that even though the loop is executing first and then the function is running after a delay of one second, how is it accessing value ‘0-9’ with let instead of 10 like var?

So this has indeed turned out to be a really interesting discussion. I looked into some more resources and was able to decipher the following for this question of yours.

We need to keep in mind when a JavaScript function is defined inside a loop with variables, it establishes something known as closures. So what closures in JS ensure is this: make the local lexical environment available to the inner function. A lexical environment comprises references to the variables within a loop or a function and is used when the same is invoked. Hence, when we initialize using let inside the loop, the inner function (which is an async callback function) knows that the next time there is a callback, the variable logged must be local and I need to update instantly or perform the “closure” of the variable instantly.

Whereas, when var is used, it’s scope is not block. This doesn’t trigger the async function for a change in the value and the variable is only “closed” after the loop has completed and hence the value 10, in your case.

I encourage you to look into these articles/answers: https://stackoverflow.com/questions/111102/how-do-javascript-closures-work
&&
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

I also believe this is one of the reasons why var is now deprecated and it’s encouraged to use let as a permanent replacement to the same.

2 Likes

Unless that block happens to be a function body. var does make a variable global. NOT using var (or let or const) means the variable is hoisted to the top of global scope even if defined inside a function.

1 Like

I believe I do understand what closures are now. In the case of var, since it leaked to the global scope and due to that the lexical environments don’t really have to do anything since the variable again is not limited to their scope. I guess that’s why the function always printed 10. However, if we look at let, then the function call inside the loop had a chain of outer lexical environments which it remembered and thus when called, they printed out the correct version of the variable instead of just printing 10.

And as @mtf said in this discussion, I’ll be coming back to this topic and thoroughly clear it out.

I have to say it’s only been a few weeks since I’ve joined the community but my learning has shot up exponentially ever since I did. I’m very grateful!

Thank you for the all the help, @selectall, @pulsating_photon and @mtf.

3 Likes

Hello. I wanted to revisit this conversation after months of working with JS and better understanding the lexical scope behavior and it’s associations with var, let, and const.

Let’s consider the same two pieces of code:

code-1 using var:

for (var val = 0; val < 10; val++) {
    console.log(val);
    setTimeout(function() {
        console.log(`The number is ${val}`);
    }, 1000);
}

and code-2 using let:

for (let i = 0; i < 10; i++) {
    console.log(i);
    setTimeout(function() {
        console.log(`The number is ${i}`);
    }, 1000);
}

Assuming I’m running the loops in global scope meaning that these loops haven’t been enclosed in a function scope, how might we better explain the behavior of these loops?

Here’s the answer:

We know how var works with respect to lexical scope. It attaches itself to a function scope or the global scope.
so the code-1 with var can be re-written as follows:

var val = 0;
for(/*nothing here*/; val<10; val++) {
  console.log(val);
  setTimeout(function printValue() {
    console.log(`The number is ${val}`);
  }, 1000);
}

This code shall output:

0
1
2
3
4
5
6
7
8
9
"The number is 10"
"The number is 10"
"The number is 10"
"The number is 10"
"The number is 10"
"The number is 10"
"The number is 10"
"The number is 10"
"The number is 10"
"The number is 10"

An important thing to remember here is that ‘The setTimeout API will only start executing the callback functions printValue once all the global synchronous code has executed’. Notice how I said “functions” in the previous line. I said that because the setTimeout API will register the printValue function as callback 10 times. The setTimeout API line of code will execute each time the loop is run and register callback function printValue to be called after 1 second from the time of registration. We won’t go deeper into how the asynchronous JS works here. So, this means that all ten calls to the printValue will be executed after loop has finished it’s execution.

And hence, the first line inside the loop console.log(val); executes 10 times and prints 0-9. Once this is done, the async code starts executing which means the setTimeout API will now push the callback function printValue on the execution stack.

printValue function creates it’s own execution context, it’s own execution scope and tries to console.log the value of the val identifier. But val identifier is not found inside it’s own execution context so it looks up the scope chain and finds itself in the global scope and there, it finds the val identifier and since the loop has already executed, the value hold by the val identifier is 10 at the moment.

So, the printValue function executes first time, prints ‘The number is 10’ and executes second time, prints ‘The number is 10’ and so on. This happens because it is referencing the same value of val identifier over and over again from the global scope.

I hope the explanation is making sense so far.

Another important detail to remember is a new scope is generated when

  1. a function starts executing
  2. a set of curly braces {} containing an identifier declaration with either let or const keyword.

These above mentioned two points shall suffice for our explanation.

So, the example below is not a new scope.

//global scope
var aValue = 5;
// pair of curly braces
{
var aValue; // this is basically a no-op but a re-declaration nonetheless.
console.log(aValue); //prints 5
}
console.log(aValue); // still prints 5

This block of code is not a new scope because it doesn’t have an identifier declaration using let or const.

But, the example below is a new scope.

var aValue = 5; // global identifier
{
let aValue = 10;
console.log(value); // prints 10
// a new scope is generated and a new identifier called value is also created and assigned value 10.
// The console.log statement looks up in it's local scope to find the identifier called 'aValue'  and finds it to have the value 10. and hence prints it.

}
// The above declared scope finished execution and hence all the declarations and memory associated with it has been "garbage collected". There's also a thing called shadowing in play here but we won't go into it's details.
console.log(aValue); // prints 5 from the global 'aValue' identifier

This block of code is a new scope since it has a identifier declaration with let.

So, now you should hopefully understand when I say this that each time the for loop with var was executed in the code-1 snippet above, a new scope was not created.

Now, let’s understand the code-2 using let:

for (let i = 0; i < 10; i++) {
    console.log(i);
    setTimeout(function printValue() {
        console.log(`The number is ${i}`);
    }, 1000);
}

We can effectively re-write the above code as follows to better understanding:
(NOTE: JS is not working exactly this way but this re-written code should help make the code more verbose as compared to the code above)

{
// a new implicit scope to hold a variable sister_i and to not pollute the global scope since let is block scoped.

let sister_i = 0;

for ( /* nothing here*/; sister_i < 10; sister_i++) {
  // here's our actual loop `i`!
  let i = sister_i;
  console.log(i);
  setTimeout(function printValue() {
    console.log(`The number is ${i}`);
  }, 1000);
}

}

So, now the rule of synchronicity still applies and all the global synchronous code will execute and the loop will run 10 times and print 0-9 and then also, register the callbacks for each time the loop is executed.

But, something has changed this time. Can you guess what has changed?

We now have a let declaration in the for loop! This changes the for loop’s behavior and thus now, each time loop is executed, a new scope is created and, for that particular scope, the callback function printValue is registered which means instead of directly going to the global scope, this time printValue has another lexical scope to look for the variable and, each time, this callback is registered, a new and unique lexical for loop scope is associated to it.

This means that the first time for loop executes, the setTimeout API registers the callback printValue with the current lexical scope having a unique identifier called i which has a value of 0. Next time the loop runs, a new scope is created again and this time, again, a new identifier called i is created and is assigned the value 1 from sister_i and this scope is associated with the registered callback for this iteration of the loop and so on.

To sum the process up, each time the loop runs, a new scope is created, a new identifier called i is created and this new scope is associated to the callback function. And therefore, when this block of code executes, we see the results as follows;

0
1
2
3
4
5
6
7
8
9
"The number is 0"
"The number is 1"
"The number is 2"
"The number is 3"
"The number is 4"
"The number is 5"
"The number is 6"
"The number is 7"
"The number is 8"
"The number is 9"

I hope this explanation helps in better understanding the behavior of var, let and const and how they interact with scopes.

2 Likes