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 bClosures 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 outsideIn 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