What is a Closure?
A closure is a function that remembers the variables from its outer scope even after that outer function has finished executing. That's it. The "closed over" variables persist as long as the inner function exists.
This sounds abstract until you see it in code. Let's start with the simplest possible example and build from there.
function makeGreeter(name) {
// 'name' is a local variable in makeGreeter
return function() {
// This inner function closes over 'name'
// It can access 'name' even after makeGreeter has returned
console.log(`Hello, ${name}!`);
};
}
const greetAli = makeGreeter("Ali");
const greetSara = makeGreeter("Sara");
greetAli(); // "Hello, Ali!"
greetSara(); // "Hello, Sara!"
// makeGreeter has finished running, but 'name' still lives
// inside each returned function — that's a closure.
Understanding Scope First
To understand closures, you need to understand lexical scope. In JavaScript, a function has access to variables defined in:
- Its own local scope
- The scope of any outer function it's nested inside
- The global scope
Closures are the mechanism by which functions carry references to their outer scope — not just at the time they were created, but for as long as they exist.
let globalVar = "I'm global";
function outer() {
let outerVar = "I'm from outer";
function inner() {
let innerVar = "I'm from inner";
// inner can access all three:
console.log(globalVar); // ✅
console.log(outerVar); // ✅ — this is the closure
console.log(innerVar); // ✅
}
inner();
}
// outer cannot access innerVar
// global cannot access outerVar or innerVar
Practical Closure Patterns
Counter Factory
function createCounter(startValue = 0) {
let count = startValue; // private — can't be accessed from outside
return {
increment: () => ++count,
decrement: () => --count,
reset: () => { count = startValue; },
getCount: () => count
};
}
const counter = createCounter(10);
counter.increment(); // 11
counter.increment(); // 12
counter.reset(); // back to 10
console.log(counter.count); // undefined — count is private!
Memoization (Caching Expensive Results)
function memoize(fn) {
const cache = {}; // closed over — persists between calls
return function(...args) {
const key = JSON.stringify(args);
if (cache[key] !== undefined) {
console.log("Returning cached result");
return cache[key];
}
cache[key] = fn(...args);
return cache[key];
};
}
const expensiveCalc = memoize((n) => {
// Imagine this takes 2 seconds
return n * n;
});
expensiveCalc(10); // calculates
expensiveCalc(10); // returns cached — instant
The Classic Closure Interview Trap
// ❌ Common bug — all buttons alert the same number
for (var i = 0; i < 3; i++) {
document.querySelectorAll("button")[i].addEventListener("click", function() {
alert(i); // always alerts 3 — i is shared!
});
}
// ✅ Fix 1: Use let (block-scoped, creates new binding per iteration)
for (let i = 0; i < 3; i++) {
buttons[i].addEventListener("click", () => alert(i)); // alerts 0, 1, 2
}
// ✅ Fix 2: IIFE (Immediately Invoked Function Expression)
for (var i = 0; i < 3; i++) {
(function(capturedI) {
buttons[capturedI].addEventListener("click", () => alert(capturedI));
})(i); // immediately invoked with current i
}