Closures
A function that remembers variables from the scope where it was created — even after that scope is gone.
ELI5
You give a kid a backpack before they leave the house. The house is gone (the function finished), but the kid still has everything in the backpack. A closure is the backpack — it carries the variables with the function wherever it goes.
Why it matters
Without closures there’s no private state, no callbacks that remember context, and no React hooks. Every event handler, timer callback, and .then() relies on closures. Understand this and async code, hooks, and module patterns click into place.
When to use it
- Private state that nothing outside can touch
- Factories that return pre-configured functions
- Callbacks that need to remember a value from when they were created
- Memoization — caching results between calls
- Counters, trackers, limiters without reaching for a class
1. Simple — counter
function makeCounter() {
let count = 0 // closed-over variable — lives in the closure
return function () {
count++ // still has access even though makeCounter() already returned
return count
}
}
const counter = makeCounter() // makeCounter runs and exits, but count survives
counter() // 1 — count is remembered
counter() // 2 — same count, incremented again
counter() // 3 — the closure keeps it alive between calls2. Intermediate — private state
function createWallet(initial) {
let balance = initial // private — only accessible through the returned methods
return {
// each method closes over the same `balance` variable
deposit(amount) { balance += amount },
withdraw(amount) { balance -= amount },
getBalance() { return balance },
}
}
const wallet = createWallet(100)
wallet.deposit(50)
wallet.getBalance() // 150 — reads the closed-over balance
wallet.balance // undefined — can't access it directly, it's private3. Advanced — memoize
function memoize(fn) {
const cache = new Map() // closed over — persists between every call to the returned function
return function (...args) {
const key = JSON.stringify(args)
if (cache.has(key)) return cache.get(key) // cache hit — closure remembered previous results
const result = fn(...args)
cache.set(key, result) // store in the closed-over cache for next time
return result
}
}
const fastSquare = memoize((n) => n * n)
fastSquare(4) // 16 — computed and cached
fastSquare(4) // 16 — returned from closure's cache, fn never runs againThis is the same pattern behind React’s useMemo.
Gotchas
The loop trap
// BROKEN — logs 3 three times
for (var i = 0; i < 3; i++) {
// all 3 callbacks close over the SAME `i` (var is function-scoped)
setTimeout(() => console.log(i), 100) // 3, 3, 3 — i is already 3 when these run
}
// FIX — use let (block-scoped, creates a NEW binding per iteration)
for (let i = 0; i < 3; i++) {
// each callback gets its OWN `i` — a fresh closure per loop iteration
setTimeout(() => console.log(i), 100) // 0, 1, 2
}Mental model
| Concept | What happens |
|---|---|
| Lexical scope | Functions see variables from where they were defined, not where they’re called |
| Closure | A function + the variables it captured from its outer scope |
| Lifetime | Closed-over variables live as long as the closure exists |
| Each call = new closure | Calling a factory twice creates two independent closures |
Last updated on