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
- a function starts executing
- 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.