Closures
A closure is a function that remembers the variables from the place where it was created, even after that place has finished running. Closures sound abstract, but you use them constantly — in event handlers, timers, callbacks and any function that returns another function.
A first closure
function makeCounter() { let count = 0; return function () { count++; return count; }; } const next = makeCounter(); console.log(next()); console.log(next()); console.log(next());
1 2 3
makeCounter finished running on line 8, yet the function it returned still reads and updates count. The inner function "closes over" the variable — it keeps a live link to it, not a copy of its value.
Each call creates a separate closure
function makeCounter() { let count = 0; return () => ++count; } const a = makeCounter(); const b = makeCounter(); a(); a(); console.log("a:", a()); console.log("b:", b());
a: 3 b: 1
Every call to makeCounter creates a fresh count. a and b each remember their own.
Private state
Variables captured by a closure can't be reached from outside, which makes closures a simple way to hide data:
function createWallet(initial) { let balance = initial; return { deposit(amount) { if (amount <= 0) throw new Error("Deposit must be positive"); balance += amount; }, getBalance() { return balance; }, }; } const wallet = createWallet(50); wallet.deposit(25); console.log(wallet.getBalance()); console.log(wallet.balance);
75 undefined
The only way to change balance is through deposit, which validates its input. This "module pattern" was the main way to get privacy before classes had #private fields (lesson 40).
Function factories
A function that builds and returns customised functions:
function multiplier(factor) { return n => n * factor; } const double = multiplier(2); const triple = multiplier(3); console.log(double(5), triple(5)); console.log([1, 2, 3].map(multiplier(10)));
10 15 [ 10, 20, 30 ]
Closures in the browser
Every event handler that uses an outer variable is a closure. Here each button's handler remembers its own count and label:
Practical closure patterns
Run only once
function once(fn) { let done = false; let result; return (...args) => { if (!done) { done = true; result = fn(...args); } return result; }; } const init = once(() => { console.log("initialising…"); return "ready"; }); console.log(init()); console.log(init());
initialising… ready ready
Memoisation (caching results)
function memoize(fn) { const cache = new Map(); return n => { if (!cache.has(n)) cache.set(n, fn(n)); return cache.get(n); }; } let calls = 0; const slowSquare = n => { calls++; return n * n; }; const fastSquare = memoize(slowSquare); fastSquare(9); fastSquare(9); fastSquare(9); console.log(fastSquare(9), "computed", calls, "time(s)");
81 computed 1 time(s)
Debouncing — waiting until a user stops typing before searching — is another classic closure, shown in lesson 47.
A note on memory
As long as a closure exists, the variables it captured stay in memory. That's usually exactly what you want. It only becomes a problem if you keep closures alive by accident — for example, event listeners on elements you've removed — while they hold on to large data.