DEV Community

Saiful Bashar
Saiful Bashar

Posted on

JavaScript Scope & Closure: Stop Memorizing, Start Understanding

Let’s be brutally honest. When you walk into a JavaScript technical interview, the interviewer isn't looking for someone who can copy-paste from Stack Overflow. They are looking for someone who understands how the machine works.

Two concepts trip up developers more than any others: Scope and Closure.

Most developers have a vague, "hand-wavy" understanding of them. They know that it works, but not why. In an interview, "hand-wavy" doesn't cut it. When you can't explain why a variable is undefined, or why a loop is printing the wrong number, you fail the test.

To master these concepts, you have to stop treating JavaScript like a black box and start thinking like the JavaScript Engine.

Here is the mental model you need to internalize.


Part 1: Scope is a Set of Rules

The first mistake developers make is thinking Scope is a physical "place" where variables live.

It’s better to think of Scope as a set of rules that the JavaScript Engine uses to figure out where to look for a variable by its identifier name.

JavaScript uses something called Lexical Scope. This is the most important term you will read today. "Lexical" relates to the lexing or parsing stage of compilation.

In plain English: Scope is determined by where you, the author, physically write the code. The nesting of your functions determines the nesting of your scopes. Once written, these rules are (mostly) set in stone before the code even runs.

The Mental Model: The Office Building

Imagine your program is a multi-story office building.

  • The ground floor (lobby) is the Global Scope.
  • Every function you declare creates a new, private floor above the one you are currently on.

When the Engine is executing code inside a function (say, on the 3rd floor) and it needs to find a variable (let's call it starthroat), it follows a strict procedure:

  1. Look locally: Does the 3rd floor have its own starthroat? If yes, use it.
  2. Look outer: If no, take the stairs down to the 2nd floor. Do they have it?
  3. Repeat: Keep going down one floor at a time until you reach the lobby (Global Scope).
  4. Give Up: If it's not in the lobby, throw a ReferenceError. It doesn't exist.

Crucial Interview Note: Scope lookups only go up (outward). They never go down (inward). The lobby cannot see what's happening on the 3rd floor.

var buildingName = "JS Towers"; // Lobby (Global)

function floorTwo() {
    var manager = "Kyle"; // 2nd Floor Scope

    function floorThree() {
        var developer = "You"; // 3rd Floor Scope

        // The engine looks here (floor 3), finds nothing.
        // Goes down to floor 2, finds 'manager'. Success.
        console.log(manager); 
    }
}

Enter fullscreen mode Exit fullscreen mode

Part 2: Closure is Not Magic

If Scope is the set of rules for lookup, Closure is what happens when you bend those rules.

Many define closure vaguely. Let's define it precisely.

Closure is observed when a function executes outside of its lexical scope, but still maintains access to that scope.

Normally, when a function finishes executing, its scope is garbage collected. The memory is freed up. Closure stops this from happening.

The Mental Model: The Backpack

If you define Function B inside of Function A, Function B gets a "hidden link" to Function A's scope surrounding it.

When Function B is passed out of Function A to be used elsewhere in your program, it doesn't leave empty-handed. It takes that hidden link with it.

Think of it like a backpack. Function B carries a backpack containing all the variables that existed in Function A when Function B was created.

Whenever Function B runs—no matter where it is, or how much later in time it is—it can open that backpack and access those variables.

function outer() {
    var secret = "XYZ_123"; // By rules of scope, this should die when outer() finishes.

    function inner() {
        // 'inner' closes over the 'secret' variable.
        // It puts 'secret' in its backpack.
        console.log("The secret is: " + secret);
    }

    return inner; // We send 'inner' out into the world.
}

// outer() runs and finishes entirely.
var myRef = outer(); 

// ... hours later ...

// myRef is executed in the global scope. 
// Yet, it still remembers the scope of 'outer'. 
myRef(); // "The secret is: XYZ_123"

Enter fullscreen mode Exit fullscreen mode

In an interview, don't just say "it remembers." Say: "Because of closure, inner retains a reference to the lexical scope of outer, preventing it from being garbage collected."


Part 3: The Classic Interview Trap

If you are asked about closure in an interview, there is a 90% chance you will see a variation of this loop problem. It is designed to test if you understand the difference between scope boundaries and value references.

The Problem:

for (var i = 1; i <= 3; i++) {
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
}

Enter fullscreen mode Exit fullscreen mode

What the junior developer expects: It prints 1, then 2, then 3, one second apart.
What actually happens: It prints 4, then 4, then 4, one second apart.

How to explain this like a pro:

  1. Identify the Scope: "The variable i is declared with var. This means it belongs to the global scope (or function scope if this were wrapped in a function), not the loop block scope."
  2. Identify the Closure: "The timer function inside setTimeout forms a closure over that single shared global variable i."
  3. The Execution: "The loop finishes running almost instantly. When it finishes, the value of i is 4 (because that's when the loop condition i <= 3 failed).
  4. The Result: "When the three timer functions finally execute seconds later, they all look into their closure backpacks, find the exact same reference to i, and print its current value: 4."

The Fix (Pre-ES6 IIFE Pattern):
You need to create a new scope for every iteration of the loop to "capture" the current value of i.

for (var i = 1; i <= 3; i++) {
    // Create an Immediately Invoked Function Expression (IIFE)
    // This creates a new scope bubble for every loop iteration.
    (function(j) { 
        setTimeout(function timer() {
            // timer now closes over 'j', which is unique to this iteration's scope
            console.log(j); 
        }, j * 1000);
    })(i); // Pass in current 'i' value
}

Enter fullscreen mode Exit fullscreen mode

(Note: In modern JS, you would just change var i to let i in the for loop head, because let creates a new block scope for every iteration automatically. Mentioning both shows historical context and modern knowledge.)


Summary

Don't memorize code snippets. Memorize the models.

  • Scope is about where variables are accessible, determined at write-time (lexically). Think of the office building floors.
  • Closure is about when variables are accessible. A function remembers its lexical scope even when executed later. Think of the backpack.

When you look at code in an interview, trace the scope lines mentally. Ask yourself: "Which scope bucket does this variable belong to?" and "What is this function carrying in its backpack?"

Do that, and you won't just pass the interview; you'll actually understand the language you use every day.

Top comments (0)