DEV Community

Idris Gadi
Idris Gadi

Posted on • Edited on

How Scope and Closures Work Together in JavaScript: A Classic Interview Question Explained

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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. With let, 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.