Closures

Course 2 · Ch 2
Closures: How Functions Remember Their Creation Scope
The mechanism behind the "nested functions see outer variables" behaviour first noticed back in Fundamentals Chapter 5

Fundamentals Chapter 5 showed a nested function reading a variable from its enclosing function, and flagged that this "remembering" behaviour is called a closure, promising full coverage later — this is that chapter. A closure is simply a function that keeps access to variables from the scope it was created in, even after that outer scope has technically finished running.

The Basic Closure

function makeGreeter(greeting) { return function(name) { console.log(`${greeting}, ${name}!`); }; } const sayHello = makeGreeter("Hello"); const sayHi = makeGreeter("Hi"); sayHello("Philip"); // "Hello, Philip!" sayHi("Sam"); // "Hi, Sam!"

makeGreeter finishes running and returns the inner function — but that inner function still remembers whatever greeting was at the moment it was created. sayHello and sayHi are two completely separate closures, each with its own private copy of greeting, even though both were built by the exact same makeGreeter function.

This Is Exactly Fundamentals Chapter 6's makeMultiplier Challenge

function makeMultiplier(factor) { return function(number) { return number * factor; }; } const double = makeMultiplier(2); console.log(double(5)); // 10 — factor (2) is still remembered

That earlier challenge asked why the inner function could still access factor after makeMultiplier had already finished — the answer is exactly this chapter's topic. The returned function "closes over" factor, carrying its own private reference to it for as long as the returned function itself exists.

A Closure Holds a Reference, Not a Snapshot

function makeCounter() { let count = 0; return function() { count++; return count; }; } const counter = makeCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3

Each call to the returned function mutates the SAME count variable — it isn't reset, and it isn't a frozen copy taken at creation time. The closure remembers the actual variable, live, the same way two object references (Fundamentals Chapter 7) point at the same underlying object rather than separate copies.

Two counters from makeCounter() are completely independent
const counterA = makeCounter(); const counterB = makeCounter(); creates two separate count variables — one per call to makeCounter() — so calling counterA() never affects counterB()'s count. Each invocation of the outer function creates a brand-new scope to close over.

The Classic Loop + Closure Pitfall (and Why let Fixes It)

for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Logs: 3, 3, 3 — NOT 0, 1, 2 (because var has no per-iteration scope) for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Logs: 0, 1, 2 — let creates a fresh i for each iteration

This is the single most famous closure-related bug in JavaScript. With var (Fundamentals Chapter 2's "avoid it" keyword), there's only ever one shared i across the entire loop — by the time any of the three setTimeout callbacks actually runs, the loop has already finished and i is 3. let creates a genuinely new i binding for each iteration, so each callback's closure captures its own separate value.

Practical Use: Private State with a Module-Style Pattern

function createBankAccount(startingBalance) { let balance = startingBalance; // not accessible from outside at all return { deposit(amount) { balance += amount; }, withdraw(amount) { balance -= amount; }, getBalance() { return balance; } }; } const account = createBankAccount(100); account.deposit(50); console.log(account.getBalance()); // 150 console.log(account.balance); // undefined — no direct access exists

balance is never a property on the returned object — there is no account.balance to read or overwrite directly. The only way to affect it is through the three methods that closed over it, giving genuine privacy with nothing extra needed: no special keyword, no class syntax (Intermediate Chapter 3) — just an ordinary closure.

This is what Chapter 8's event listeners rely on too
Every addEventListener callback from Fundamentals Chapter 8 is itself a closure — it routinely reaches back to variables declared outside the handler (a counter, a piece of state) and keeps working correctly across repeated clicks, for exactly the same underlying reason as makeCounter above.

Coding Challenges

Challenge 1

Write a function makePowerOf(exponent) that returns a function taking a base number and returning base raised to that exponent (use ** for exponentiation). Create square = makePowerOf(2) and cube = makePowerOf(3), then test both with the same input.

📄 View solution
Challenge 2

Write a function makeIdGenerator() that returns a function with no parameters, returning a new sequential ID each time it's called (starting at 1). Create two SEPARATE generators and call each one a few times to prove their counts don't interfere with each other.

📄 View solution
Challenge 3

Write a function createInventory() returning an object with addItem(name, qty), removeItem(name), and getCount(name) methods, all closing over a private object that the caller can never access directly. Add a few items, remove one, and confirm getCount reflects the changes.

📄 View solution

Chapter 2 Quick Reference

  • Closure — a function that retains access to variables from its creation scope, even after that scope has "finished"
  • Each call to the outer function creates a fresh, independent set of variables to close over
  • A closure references the real variable, not a frozen snapshot — mutations are visible across calls
  • var in a loop + closures = classic bug; let gives each iteration its own binding, fixing it
  • Private state pattern: variables declared inside an outer function, exposed only via returned methods
  • Event listeners are closures — they routinely reach back to outer variables across repeated events
  • Next chapter: ES6 classes — constructors, methods, and inheritance with extends