Closures
A closure is a function that remembers the variables from the scope where it was created, even after that scope has finished running. It sounds abstract, but you have probably been creating closures since your first event handler. Closures are the single most powerful tool in JavaScript for hiding state, building factories, and writing modular code without classes.
The simplest example
function makeAdder(x) {
return function (y) {
return x + y; // remembers `x` from the outer call
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(3)); // 8
console.log(add10(3)); // 13
console.log(add5(100)); // 105makeAdder has finished running by the time we call add5(3). Its local variable x should be gone — yet it is not. The inner function "closed over" x and keeps it alive as long as add5 itself is alive.
A counter with private state
Closures are the classic way to keep data private without using a class. The outer function holds the state; only the returned functions can touch it.
function createCounter(start = 0) {
let count = start;
return {
inc: () => ++count,
dec: () => --count,
value: () => count,
reset: () => { count = start; },
};
}
const c = createCounter(10);
console.log(c.value()); // 10
c.inc(); c.inc(); c.inc();
console.log(c.value()); // 13
c.reset();
console.log(c.value()); // 10Nothing outside has access to count. There is no c.count property — the only way to interact with the state is through the four methods.
Closures in event handlers
Every time you attach an event listener that reads outer variables, you are creating a closure.
function bindButton(button, label) {
button.addEventListener("click", () => {
console.log("clicked: " + label); // closes over `label`
});
}
// Calling bindButton(btn1, "save") and bindButton(btn2, "delete")
// gives each button its own closure with its own `label`.The classic loop bug
This is the famous gotcha that taught a generation of developers what closures actually do.
The bug — with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3var is function-scoped, so all three callbacks close over the same i. By the time the timers fire, the loop is finished and i is 3.
The fix — with let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 0, 1, 2let is block-scoped: each iteration of the loop creates a new binding of i, so each callback closes over its own copy.
Closures for "configured" functions
Returning a function pre-bound with config is one of the most common uses in real code: validators, formatters, fetchers, debouncers.
function makeFormatter(prefix) {
return value => prefix + " " + value;
}
const tag = makeFormatter("[INFO]");
console.log(tag("user logged in")); // [INFO] user logged in
console.log(tag("saving file")); // [INFO] saving fileA real debounce
A debounce delays calling a function until it has stopped being called for some time. The "memory" of the pending timer is held in a closure.
function debounce(fn, ms) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
const search = debounce(q => console.log("searching for", q), 300);
search("a"); search("ab"); search("abc");
// After ~300ms of silence, prints: searching for abcMemory implications
Only the variables actually used by the inner function are typically kept alive — modern engines are smart about this, but do not bet your life on it.
Detaching event listeners (
removeEventListener) drops the closure they hold.If you see a memory leak in a long-running app, suspect a closure holding onto something it shouldn't.