Today, we will dive deep into a problem that is frequently presented in JavaScript interviews. Since my interaction with this problem, I've struggled quite a bit to understand what's going on behind the scenes. I looked for solutions online, but unfortunately, there wasn't a single solution that satisfied all my questions.
So, I started exploring the questions and found the answers that I've presented in this article. Hopefully, this discussion will clear up any confusion you may have on this topic as well. Let's get started.
Variables created with ‘var’ act weird
for (var i = 1; i < 5; i++) {
setTimeout(() => {
console.log(i); // 5 (4 times)
}, 1000);
}
console.log(i); // 5
You might have encountered this problem already, where the above for
loop logs '5' four times in the console instead of logging '1' through '4'. The key factor contributing to this behavior is that variables created with var
are function-scoped.
Behind the scenes
In the above code, in each iteration of the loop, a block is created with a variable i
, and a setTimeout
function gets registered in the web API environment. The setTimeout
function schedules a callback function that references the variable i
within its scope to execute after a one-second delay.
Important to note that the variable i
, being created with var
, doesn't respect the block and becomes available in the global scope, making it accessible both inside and outside of the for
loop.
Dynamics
In the first iteration of the loop, a variable i
is created with the value of '1'. In the second iteration, another variable i
is created with the value of '2'. Since both variables exist in the same scope, the value of the previous variable i
gets modified to '2'.
Likewise, in the last iteration of the loop, another variable i
is created with the value of '5', which modifies the value of the previous variable i
to '5'. At this point, there is a single variable i
in the scope with the value of '5'.
Notice that, even though the loop terminates when the value of i
is '4', we still get '5' as the final value. This is because the value of i
is already modified to '5' before the JavaScript engine reaches the loop termination logic.
Finally, as soon as the synchronous code is executed, the callback functions start executing. Each callback function executes, finds the value of i
as '5', and logs it to the console. That's how the above for
loop logs '5' four times instead of logging '1' through '4'.
Variables created with ‘let’ fix it
One way to fix this issue is to change the variable declaration to let
.
for (let i = 1; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
console.log(i); // ReferenceError: i is not defined
Notice that, as soon as we've changed the variable declaration from var
to let
, the variable i
is no longer accessible outside of the for
loop. This is because, unlike var
, variables created with let
are block-scoped and only accessible within the block they are created.
Behind the scenes
Identical to the solution using var
, in each iteration of the loop, a block is created with a variable i
, and a setTimeout
function gets registered in the web API environment. The setTimeout
function schedules a callback function that references the variable i
within its scope to execute after a one-second delay.
However, the difference is made by the usage of let
in this solution. Since let
is block-scoped, a variable created with let
in the current scope cannot access or modify another variable created with let
from the scope created in the previous iteration. This results in each scope having a unique variable value for i
.
Finally, when the callback functions execute, each function refers to the variable i
from its respective scope and logs the value to the console. This effectively fixes the weird behavior introduced by using var
.
That's it. Hopefully, you now know the reasons behind this problem and how to fix it. Along with this learning, you now also have another reason not to use var
anymore to avoid unexpected behaviors like this.
By the way, if you are interested in a deeper dive into the concepts of scope and how synchronous and asynchronous code is executed in JavaScript, I have articles on Exploring the scope and scope chain in JavaScript and Exploring the inner workings of a JavaScript runtime that provide further insights.