CodeOath
← All posts
JavaScript75 min total · 15 parts

JavaScript Core Concepts: Scope, Closures, `this`, and the Event Loop

Contents — Part 5 of 15: Closures: A Function Remembers Where It Was Created
Part 5 of 15 · ~2 min

Closures: A Function Remembers Where It Was Created

A closure is what happens when an inner function keeps access to variables from its enclosing scope, even after that outer function has already finished running and returned.

function makeCounter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

const counterA = makeCounter();
const counterB = makeCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 — a completely separate closure, separate count

Each call to makeCounter() creates a fresh count variable, and the returned function keeps a live reference to that specific count — not a copy of its value at the time of return. counterA and counterB never interfere with each other because each call created its own independent closure. This is the mechanism behind private state in JavaScript (long before classes had real private #fields), memoization, and debounce/throttle utilities.

Closures capture variables, not values

The single most important thing to internalize: a closure closes over the variable itself, not a snapshot of its value at the time the inner function was created. This is exactly what causes the classic loop bug:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// logs: 3 3 3 — there is only ONE shared `i` (var is function-scoped),
// and by the time any callback runs, the loop has already finished with i = 3
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// logs: 0 1 2 — `let` creates a FRESH binding of i for every single iteration,
// so each closure captures its own separate i

Before let existed, the fix was manufacturing a new scope per iteration manually, usually with an IIFE (immediately invoked function expression):

for (var i = 0; i < 3; i++) {
  (function (capturedI) {
    setTimeout(() => console.log(capturedI), 0);
  })(i);
}
// logs: 0 1 2 — each IIFE call creates a new scope with its own capturedI

let in a for loop essentially does this automatically, which is one of the main reasons it replaced var in modern code.

Closures and memory

Because a closure keeps its enclosing scope alive, holding onto a closure for a long time (storing it in a global array, an event listener that's never removed) keeps every variable it captured alive too, even ones the closure doesn't actually use if they're declared in the same scope. In long-lived applications (a single-page app that never does a full page reload) this is a genuine, if usually minor, source of memory growth — it's one more reason cleaning up subscriptions and event listeners (covered in the async section below) matters in practice, not just in principle.