Closures
A closure is a function that keeps access to variables from the scope where it was created, even after that scope has finished.
A closure, step by step
function makeCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
makeCounter()runs.A new local binding named
countis created.- It returns
increment.The inner function closes over the binding, not a frozen copy of its value.
- The outer call finishes.
Normally its locals could be discarded, but
incrementstill referencescount. - Each call updates the same binding.
That is why the results progress from
1to2.
Each outer call creates new state
Calling the factory twice creates two separate lexical environments. The counters do not interfere with each other.
const likes = makeCounter();
const shares = makeCounter();
likes(); // 1
likes(); // 2
shares(); // 1, its own count
What closures are good for
Private state
Expose a small public API while keeping the underlying value inaccessible from the outside.
function createWallet(startingBalance) {
let balance = startingBalance;
return {
deposit(amount) { balance += amount; },
getBalance() { return balance; }
};
}
const wallet = createWallet(100);
wallet.deposit(50);
wallet.getBalance(); // 150
wallet.balance; // undefined
Function configuration
Create specialized functions without repeatedly passing the same input.
function multiplyBy(factor) {
return number => number * factor;
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
double(8); // 16
triple(8); // 24
Callbacks and event handlers
A callback can retain context from the code that registered it.
function setupButton(button, message) {
button.addEventListener("click", () => {
console.log(message);
});
}
setupButton(saveButton, "Saved!");
// The handler remembers message after setupButton returns.
Debouncing
Delay a function until calls stop arriving. The returned function closes over timerId, so every call can cancel the previous timer.
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const search = debounce(query => {
console.log(query);
}, 300);
search("j");
search("ja");
search("javascript"); // only this call runs
Throttling
Limit a function to one call per interval. The returned function closes over ready, which records whether another call is allowed.
function throttle(fn, interval) {
let ready = true;
return function (...args) {
if (!ready) return;
ready = false;
fn.apply(this, args);
setTimeout(() => {
ready = true;
}, interval);
};
}
const trackScroll = throttle(() => {
console.log("scroll position recorded");
}, 200);
The classic loop trap
var creates one function-scoped binding, so every callback shares it. let creates a fresh binding for each iteration.
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 3, 3, 3
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 0, 1, 2
The callbacks run after the loop. By then the single var i binding contains 3; the three let i bindings still contain 0, 1, and 2.
Closures and memory
A closure keeps referenced values reachable. This is useful, but long-lived listeners, timers, or caches can unintentionally retain large objects.
- Remove event listeners and clear timers when they are no longer needed.
- Capture only the data the callback actually needs.
- Do not treat every closure as a leak; retained state is often the point.
Check your understanding
Predict each result before opening the answer.
-
What does this print?
let label = "before"; function showLabel() { console.log(label); } label = "after"; showLabel();Show answer
"after". The closure references thelabelbinding, not the value it held when the function was defined. -
What are the two results?
function makeAdder(amount) { return value => value + amount; } const addFive = makeAdder(5); const addTen = makeAdder(10); addFive(2); addTen(2);Show answer
7and12. Each call tomakeAddercreates a newamountbinding, so each returned function closes over different state. -
Can outside code set
secretdirectly?function createSecret() { let secret = "orbit"; return () => secret; } const reveal = createSecret();Show answer
No.
secretis outside the caller's scope. Only the returned function has access to it, which makes closure-based data privacy possible.