beginner 6 min read
Closures
A closure is a function bundled together with the lexical scope it was created in, so it keeps access to those outer variables even after the outer function has returned.
Why it matters in interviews
Closures are how JavaScript gives you private state without classes: counters, memoized caches, event handlers with captured context, and module patterns all rely on a function remembering variables from where it was defined, not from where it's called.
Visual & Interactive Explanation
Interactive Flow Execution
1
Outer function runs
creates local variables
2
Inner function is defined
captures the outer scope by reference
3
Outer function returns
normally its scope would be discarded
4
Inner function still holds it
closure keeps the scope alive
Code Examples & Implementation
Counter via closure
function createCounter() {
let count = 0;
return {
increment: () => ++count,
get value() {
return count;
},
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.value); // 2 — "count" is private, only reachable via the closureThe classic loop pitfall
// var is function-scoped: all three callbacks share ONE binding
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}
// let is block-scoped: each iteration gets its own binding
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // 0, 1, 2
}Interview-Ready Answers
A function that remembers the variables from its outer scope, even after that outer function has finished running.
Official Documentation & Specifications
Follow-up Questions & Deep Dives
🎯 Code Output — What Does This Print?
Question 1medium
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}Question 2medium
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}Question 3hard
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count,
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount());
console.log(counter.increment());Question 4tricky
function outer() {
var x = 10;
function inner() {
console.log(x);
var x = 20;
}
inner();
}
outer();Question 5hard
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(
(function(j) {
return () => console.log(j);
})(i)
);
}
funcs[0]();
funcs[1]();
funcs[2]();Common Mistakes & Anti-Patterns
- Using var inside loops that create callbacks, expecting per-iteration values
- Assuming closures deep-copy the variables they capture
- Not clearing intervals/listeners that hold closures over large data
Implementation Evolution: Anti-Pattern to Production
✕
❌ Anti-Pattern: Global State Pollution & Stale Callback Capture
// ❌ Broken: Global variable shared across all handlers, leading to race conditions
let activeTimer;
function debounce(fn, delay) {
return function(...args) {
clearTimeout(activeTimer);
activeTimer = setTimeout(() => fn(...args), delay);
};
}Why this breaks: Using an outer module-level variable means multiple debounced functions overwrite each other's timers. Invoking debounce on search and scroll simultaneously cancels whichever timer was scheduled first.
✓
✅ Correct Pattern: Encapsulated Closure Binding
// ✅ Correct: Private timer instance per debounced function instance
function debounce(fn, delay) {
let timerId;
return function(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}🚀
🚀 Production-Grade (Hardened): Production-Grade Closure with Cancel & Flush Methods
// 🚀 Production: TypeScript typed, preserves 'this', exposes .cancel() & .flush()
export function debounce<T extends (...args: any[]) => any>(
fn: T,
wait: number,
options: { leading?: boolean } = {}
) {
let timerId: ReturnType<typeof setTimeout> | undefined;
let lastArgs: Parameters<T> | undefined;
let lastThis: any;
function debounced(this: any, ...args: Parameters<T>) {
lastArgs = args;
lastThis = this;
const callNow = options.leading && !timerId;
clearTimeout(timerId);
timerId = setTimeout(() => {
timerId = undefined;
if (!options.leading && lastArgs) {
fn.apply(lastThis, lastArgs);
}
}, wait);
if (callNow) {
fn.apply(lastThis, lastArgs);
}
}
debounced.cancel = () => {
clearTimeout(timerId);
timerId = undefined;
lastArgs = undefined;
};
debounced.flush = () => {
if (timerId && lastArgs) {
fn.apply(lastThis, lastArgs);
debounced.cancel();
}
};
return debounced;
}⚠️ Trick Questions & Interviewer Traps
Q1:Do closures in JavaScript capture variables by value or by reference?
Q2:Can two separate inner functions share the exact same closure environment?
📋 Rapid Revision Cheat Sheet
- Closure = Function + Lexical Scope reference (not a snapshot copy).
- Live bindings: Modifying captured variables reflects in all sibling closures.
- Loop trap: 'var' shares 1 function-scoped binding; 'let' creates N per-iteration bindings.
- Memory leak trigger: Dangling listeners/timers holding closures to large objects.
- V8 internals: Escaping variables migrate from Stack to Heap Context.
Real-World Architectural Scenario
You need a debounce utility that remembers the last timer across calls. How does a closure make this possible without a class?
Rate Your Readiness
Rate how comfortably you can explain this in an interview to update your global readiness gauge:
Rate your confidence: