ToolPopToolPop
JavaScript · Lesson 10 of 18

Scope, lexical environment, and closures (the deep version)

7 min readUpdated 24 Jun 2026

Closures are the question every senior interviewer reaches for when they want to separate "I learned JS from a YouTube playlist" from "I actually understand JS". The good news is the concept is not complicated. The bad news is most explanations make it sound mystical.

Forget the mysticism. A closure is a function plus the memory record it was born next to. That is the whole thing. Let's unpack the parts.

Lexical scope, written not called

Lexical scope means JS decides what variables a function can see based on where you wrote the function in the source code, not where you called it from.

js
const city = "Bengaluru";
 
function showCity() {
  console.log(city);
}
 
function caller() {
  const city = "Mumbai";
  showCity();
}
 
caller(); // Bengaluru, not Mumbai

showCity was written next to the global city, so it sees "Bengaluru". It does not matter that caller had its own city. The link was decided at write time.

That link is called the scope chain. Inner scope first, then outer, then outer's outer, then global. JS walks this chain looking for the name.

Diagram
rendering diagram...
Scope chain lookup, inner to outer

Every execution context carries a reference called the lexical environment. It is just a record of, "here are my variables, and here is a pointer to my parent's record." That parent pointer is what makes the chain.

Closures, the chai-stall cash-box

Imagine a chai-stall owner who gives his cousin a small cash-box and sends him out to deliver chai to offices. The cousin walks away from the stall, but the cash-box goes with him. He can still take payments because he is carrying that one box.

A closure is the same. A function that gets returned from another function carries its outer lexical environment with it, even after the outer function has finished.

js
function makeWallet(initial) {
  let balance = initial;
  return function spend(amount) {
    balance -= amount;
    return balance;
  };
}
 
const wallet = makeWallet(1000);
wallet(150); // 850
wallet(200); // 650

Step-by-step.

  1. Step 1. makeWallet(1000) is called. New EC pushed. balance = 1000.
  2. Step 2. It returns the inner spend function. The outer EC pops off the stack.
  3. Step 3. Normally balance would be garbage-collected. But spend still references it, so JS keeps that lexical environment alive.
  4. Step 4. wallet(150) calls spend. It walks the scope chain, finds balance in the preserved environment, mutates it.
  5. Step 5. Next call sees the updated 850.

Interview trap: a closure does not snapshot values. It holds a live reference. Mutate balance and every call to spend sees the new value.

The famous for var setTimeout trap

This one shows up in every interview, every year.

js
for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log("Order", i), 100);
}
// Output: Order 4, Order 4, Order 4

Why? var is function-scoped, so there is only one i shared by all three callbacks. By the time the timers fire, the loop has finished and i is 4. All three closures point at the same i.

Fix with let.

js
for (let i = 1; i <= 3; i++) {
  setTimeout(() => console.log("Order", i), 100);
}
// Output: Order 1, Order 2, Order 3

let is block-scoped. Each iteration of the loop creates a fresh i in a new lexical environment. Each callback closes over its own copy. This is not a bug fix, it is JS doing exactly what the spec says.

Closures and memory, the leak nobody talks about

A closure only keeps the variables it actually references. JS engines are smart about this. But you can still leak.

js
function loadOrders() {
  const huge = new Array(1_000_000).fill("order");
  return function getCount() {
    return huge.length;
  };
}
 
const counter = loadOrders(); // huge stays in memory forever

huge is referenced by getCount, so the million-element array cannot be collected. If you only need the length, pull it out before returning.

js
function loadOrders() {
  const huge = new Array(1_000_000).fill("order");
  const length = huge.length;
  return function getCount() { return length; };
}

Now huge is unreferenced after loadOrders returns and the garbage collector can free it.

Quick reference

Lexical Scope
Variable visibility decided by where a function is written in the source code.
Scope Chain
The ordered list of lexical environments JS walks to resolve a variable name.
Lexical Environment
The memory record holding a scope's variables plus a pointer to its parent scope.
Closure
A function bundled with the lexical environment it was defined in, so it can access outer variables even after the outer function has returned.

Predict the output

js
function counter() {
  let n = 0;
  return () => ++n;
}
const a = counter();
const b = counter();
console.log(a(), a(), b()); // ?

Each counter() call creates a fresh n in a fresh lexical environment. a and b have separate boxes. Output is 1 2 1.

Senior rule: a closure is a function plus the lexical environment it was born in. Not a snapshot, a live reference.

Quick quiz, prove you got it

0/3 answered
  1. 1.A closure remembers...
  2. 2.What does this print? `for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);`
  3. 3.Does a closure prevent the entire outer scope from being garbage collected?

Interview insights

Closure interview answer

  • Definition: function + lexical environment in which it was defined
  • A real example (counter or once or private state)
  • The for-var loop trap and how let fixes it
  • Memory consideration: closures can hold large data alive

takeaway: Closure is not "function inside a function". It is the linked environment that persists.

Tricky questions they will ask

Q.Why does this print 1 then 1, not 1 then 2? `function f() { let n = 1; return () => console.log(n); } const a = f(); const b = f(); a(); b();`

A.Each call to f() creates a new lexical environment. a and b are TWO separate closures, each over its OWN n. They look the same but are independent.

Q.When does a closure actually leak memory?
common wrong answer:Whenever you use a closure

A.When the closure captures a large object that the inner function does not even use. Modern engines try to optimise this away, but if you assign the closure to a long-lived variable (a DOM listener, a global, a timer), the captured data stays alive as long as the closure does.

Free tools you can use while you learn

Common questions

Q.In one sentence, what is a closure?
A.A function plus the lexical environment in which it was defined. Even after the outer function returns, the inner function can still access the outer variables it referenced.
Q.Does a closure hold the entire outer scope in memory?
A.No. The JS engine keeps only the variables the inner function actually references. Unreferenced variables in the outer scope can still be garbage-collected.
Chai0/2 done

Watching quietly. Tap me if you want a tip.

JS Playground
console
// hit run to see output

Try this (0 of 2 done)

  1. 1

    Make the loop log 0, 1, 2 (not 3, 3, 3) by changing var to let.

    show answer
    for (let i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 0);
    }
  2. 2

    Build a private counter using a closure. Call it 3 times and log results.

    show answer
    function makeCounter() {
      let n = 0;
      return () => ++n;
    }
    const tick = makeCounter();
    console.log(tick(), tick(), tick());