Closures

JavaScript · Practical · 12 min

Closures power callbacks, event handlers, module patterns, memoisation and React hooks.

A closure is a function together with the variables from the scope where it was created. In JavaScript every function is a closure: when it runs later, it can still read and update the variables that were in scope at its definition, even after the outer function has returned.

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

Each call to makeCounter() creates a fresh count and a function that closes over it. Nothing else can reach that variable — closures give you private state without classes.

Closures capture variables, not values. If the variable changes later, the closure sees the new value. That's the source of the classic loop bug:

for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
// 3, 3, 3

There is only one var i, and by the time the callbacks run it's 3. With let, each iteration gets a new binding, so you get 0, 1, 2.

Typical uses:

  • Factories that configure a function: const add5 = makeAdder(5).
  • Callbacks and handlers that need context from where they were set up.
  • Memoisation: keep a cache in the enclosing scope.
  • Once/debounce wrappers that track calls between invocations.

A closure keeps everything it references alive. A long-lived handler that closes over a large array keeps that array in memory — a common source of leaks, covered in the garbage-collection lesson.

Example

function makeCounter(start = 0) {
  let count = start;                 // private to this counter
  return {
    increment: () => ++count,
    reset: () => { count = start; },
  };
}
const a = makeCounter();
const b = makeCounter(10);
a.increment(); a.increment();
console.log(a.increment(), b.increment()); // 3 11

function memoize(fn) {
  const cache = new Map();           // lives as long as the returned function
  return (n) => {
    if (!cache.has(n)) cache.set(n, fn(n));
    return cache.get(n);
  };
}
const slowSquare = n => { console.log("computing", n); return n * n; };
const fastSquare = memoize(slowSquare);
fastSquare(4);                       // computing 4
console.log(fastSquare(4));          // 16 (cached, no log)

Remember

  • A closure is a function plus the variables in scope where it was defined
  • Closures capture variables, not snapshots of values
  • Use them for private state, factories and caches

Watch out for

  • Closing over a var loop variable in async callbacks — use let.

Check yourself

  1. for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); — what is printed?
  2. Why can't outside code change count in makeCounter's returned functions?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures