Scope and Closures are two fundamental concepts in JavaScript that often elude even experienced developers, and what’s frequently overlooked is that these two concepts don’t operate in isolation; they usually work hand in hand in real-world JavaScript code.
In this article, we’ll explore how Scope and Closures come into play by breaking down a classic for
loop interview question.
Prerequisite
Before diving in, you should have a basic understanding of:
The Interview Question
Here’s that classic for
loop interview question that often trips developers up:
Question: What will be the output of the following code?
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
A developer unfamiliar with how scope and closures work together might say it will log 0
, 1
, 2
. Those who understand the behaviour correctly will answer that it logs 3
, 3
, 3
.
Follow-up Question: How would you fix this bug?
The Solution
The most common solution is to replace the var keyword with let:
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
This change works, but do you understand why it works? That’s where Scope and Closures come into play.
Understanding the Scope
To fully understand why this happens, we need to take a step back and consider where the for
loop lives. In real-world code, a for
loop almost always exists inside a function.
The key point in the original snippet is that var
is function-scoped. Even though i
is declared in the for
loop declaration, it’s scoped to the entire function, not to each iteration of the loop.
We can rewrite the code like this, and the behaviour remains exactly the same:
function main() {
var i = 0;
{
setTimeout(() => {
console.log("value", i);
}, 100);
i++;
}
{
setTimeout(() => {
console.log("value", i);
}, 100);
i++;
}
{
setTimeout(() => {
console.log("value", i);
}, 100);
i++;
}
}
main();
Here, you can see that every setTimeout
callback closes over the same i
variable. In JavaScript, closures don’t copy the value of a variable. Instead, they keep a reference to the original variable. So when the callbacks eventually run, they all see whatever value i
has at that moment.
Since the loop has already completed by the time the setTimeout
callbacks fire, i
has been incremented to 3
. That’s why you see 3
printed three times.
How let
Solves the Problem
When we replace var
with let
, we’re essentially changing how the variable is scoped and initialised during each iteration of the loop.
Unlike var
, let
is block-scoped. The for
loop creates a new block for each iteration, and let
creates a new binding of i
every time. This is roughly equivalent to writing:
function main() {
{
let i = 0;
setTimeout(() => {
console.log("value", i);
}, 100);
}
{
let i = 1;
setTimeout(() => {
console.log("value", i);
}, 100);
}
{
let i = 2;
setTimeout(() => {
console.log("value", i);
}, 100);
}
}
main();
If you look closely, each setTimeout
still closes over i
, but now i
is a different variable in each block, initialised with the correct value for that iteration. That’s why we get the expected output: 0
, 1
, 2
.
How Closure and Scope Work Together
At the heart of this problem is how Scope and Closures interact.
Scope determines where a variable is accessible in the code. With
var
, the variable is function-scoped, meaning there's only one shared variable throughout the loop. Withlet
, a new block-scope variable is created for each iteration.Closures happen when a function "remembers" variables from its surrounding scope, even after that scope has finished executing. In this example, each setTimeout callback forms a closure over
i
.
When using var
, all the closures refer to the same i
, and since the loop finishes before the timeouts execute, they all log 3
.
When using let
, a new i
is created for each iteration, so each closure captures its own distinct copy of i
. That’s why the output becomes 0
, 1
, 2
.
Acknowledgements
This article is heavily inspired by Kyle Simpson's "Scope & Closures" from the "You Don’t Know JS" (YDKJS) series.
Even though I technically knew the answer to this interview question, I struggled to explain why it works that way. That struggle made me realise I didn’t fully understand what was actually happening. The book helped me finally connect the dots.
Conclusion
Understanding how Scope and Closures work together is a crucial part of JavaScript, and struggling with interview questions like this one is a great nudge in the right direction to deepen that understanding.
If you enjoyed this article, feel free to connect with me on LinkedIn, follow me on X/Twitter, or find me on Bluesky. I regularly share posts like this and share lessons from my learning journey.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.