10 / 20 · Day 4
Day 4 · Concept 10

Closures & scope

A function "closes over" the variables in scope when it was defined. The variables stay alive as long as the function does. This single mechanism powers callbacks, modules, currying, and event handlers.


1 · The simplest closure

js closure.js
function makeCounter() {
    let count = 0;
    return function() {
        count += 1;
        return count;
    };
}

const c = makeCounter();
console.log(c());   // 1
console.log(c());   // 2
console.log(c());   // 3

const d = makeCounter();   // fresh count
console.log(d());           // 1

count doesn't disappear when makeCounter returns. The returned function still holds a reference. That reference keeps count alive — and private to this counter.

2 · Private state via closure

js private.js
function makeBank(initial) {
    let balance = initial;
    return {
        deposit(n) { balance += n; },
        withdraw(n) {
            if (n > balance) throw new Error("insufficient");
            balance -= n;
        },
        balance: () => balance,
    };
}

const acct = makeBank(100);
acct.deposit(50);
acct.withdraw(30);
console.log(acct.balance());     // 120
// acct.balance is NOT directly accessible — it's locked inside the closure

3 · The classic var trap

js trap.js
// What it looks like — and the bug
const fns = [];
for (var i = 0; i < 3; i++) {
    fns.push(() => i);     // 'i' is function-scoped — shared!
}
console.log(fns.map(f => f()));   // [3, 3, 3]

// With let — block-scoped, fresh per iteration
const fns2 = [];
for (let i = 0; i < 3; i++) {
    fns2.push(() => i);
}
console.log(fns2.map(f => f()));  // [0, 1, 2]

The reason "never var" exists. Block-scoping with let avoids the trap entirely.

4 · Currying — closures applied

js curry.js
const multiply = a => b => a * b;
const double = multiply(2);
const triple = multiply(3);

console.log(double(5));   // 10
console.log(triple(5));   // 15

// Useful for callbacks
const items = [1, 2, 3];
console.log(items.map(multiply(10)));   // [10, 20, 30]

5 · Memory considerations

Closures hold references to their enclosing scope. If a long-lived closure (an event handler) captures a big object, the object can't be garbage collected. Watch for this in React effects or long-running timers — null out references you don't need.

6 · Common mistakes

  • The var-in-loop trap. Use let.
  • Stale closures in React. useEffect captures the variables at render time. Use refs or include in the deps array.
  • Heavy captures. A closure capturing a 50-MB object keeps it alive — be mindful in long-lived contexts.

7 · When it clicks

  • You see "function returns function" and instantly know it's closure-based state.
  • You spot memory leaks from over-capturing.
  • Currying and partial application feel natural rather than clever.
Found this useful?