JavaScript by Example

Closures

Functions that remember the variables from their defining scope - the backbone of private state, memoization, and the module pattern.

A closure is a function bundled together with the variables from its surrounding lexical scope. Every function in JavaScript is a closure - the interesting cases are when that function outlives its defining scope.

A counter factory returns a function that captures a private count variable. Each call to makeCounter creates an independent count - callers cannot access or reset it directly.

function makeCounter(start = 0) {
  let count = start;
  return function () {
    count++;
    return count;
  };
}
 
const a = makeCounter();
const b = makeCounter(10);
 
console.log(a()); // 1
console.log(a()); // 2
console.log(b()); // 11
console.log(a()); // 3 - independent of b

Closures are the natural fit for memoization: a wrapper function closes over a Map to cache expensive results without exposing the cache to callers.

function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}
 
function slowSquare(n) {
  // simulate expensive work
  return n * n;
}
 
const fastSquare = memoize(slowSquare);
console.log(fastSquare(4));  // 16 (computed)
console.log(fastSquare(4));  // 16 (cached)
console.log(fastSquare(5));  // 25 (computed)

The module pattern uses an IIFE to create a private scope and return only the public API - a lightweight alternative to ES modules when you need to hide implementation details in a single file.

const cart = (() => {
  const items = [];
 
  return {
    add(item) {
      items.push(item);
    },
    remove(name) {
      const idx = items.findIndex((i) => i.name === name);
      if (idx !== -1) items.splice(idx, 1);
    },
    total() {
      return items.reduce((sum, i) => sum + i.price, 0);
    },
  };
})();
 
cart.add({ name: "book", price: 12 });
cart.add({ name: "pen", price: 2 });
console.log(cart.total()); // 14
// `items` is not accessible from outside

In production

Closures that capture large objects keep those objects alive in memory for as long as the closure itself is reachable. In SPAs, this is a common leak source: an event handler attached to a button captures the entire component state or a large API response, and it stays in memory until the handler is explicitly removed. The fix is to copy only the field you need - const id = user.id - rather than closing over user. The same principle applies to Node.js request handlers that capture req inside a callback that persists beyond the request lifecycle.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free