Closures
The most important concept in JavaScript — how functions remember their outer scope
Last updated on
Closures are the #1 most asked topic in JavaScript interviews. Once you understand closures, you understand half of JavaScript.
"A closure is a function that remembers the variables from its outer scope, even after the outer function has finished executing."
The Simplest Closure
function outer() {
const message = "Hello"; // this variable lives in outer's scope
function inner() {
console.log(message); // inner "closes over" message
}
return inner;
}
const fn = outer(); // outer() finishes executing
fn(); // "Hello" ← but inner still remembers `message`!What happened? When outer() finished, normally its variables would be garbage collected. But inner still references message, so JavaScript keeps it alive. This retained reference is a closure.
How Closures Work — Mental Model
outer() creates:
┌───────────────────────────┐
│ message = "Hello" │
│ │
│ inner() ─── closure ───► │ (inner holds a reference)
│ │
└───────────────────────────┘
↑
│
Even after outer() returns,
this environment stays alive
because inner() still uses itReal-World Use Cases
1. Data Privacy (Private Variables)
function createBankAccount(initialBalance) {
let balance = initialBalance; // private — no direct access
return {
deposit(amount) {
if (amount <= 0) throw new Error("Invalid amount");
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.withdraw(200); // 1300
account.getBalance(); // 1300
account.balance; // undefined ← truly private!2. Function Factories
function createMultiplier(factor) {
return (number) => number * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const toPercent = createMultiplier(100);
double(5); // 10
triple(5); // 15
toPercent(0.5); // 50Each function "remembers" its own factor.
3. 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("From cache");
return cache[key];
}
console.log("Computing...");
const result = fn(...args);
cache[key] = result;
return result;
};
}
const expensiveAdd = memoize((a, b) => {
// simulate heavy computation
return a + b;
});
expensiveAdd(1, 2); // "Computing..." → 3
expensiveAdd(1, 2); // "From cache" → 34. Event Handlers
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
button.addEventListener("click", () => {
// This callback closes over `message`
alert(message);
});
}
setupButton("btn1", "Welcome!");
setupButton("btn2", "Goodbye!");
// Each button has its own `message` in its closure5. React Hooks (useState)
Simplified version of how React's useState works under the hood:
function useState(initialValue) {
let state = initialValue; // closed over
function getState() {
return state;
}
function setState(newValue) {
state = newValue;
// trigger re-render...
}
return [getState, setState];
}
const [getCount, setCount] = useState(0);
getCount(); // 0
setCount(5);
getCount(); // 5The Famous Loop Problem
The Bug
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// Output: 3, 3, 3Why? var is function-scoped, so there's only ONE i. All three callbacks close over the SAME i, which is 3 after the loop ends.
Fix 1: Use let (Block Scope)
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// Output: 0, 1, 2Each iteration gets its own i.
Fix 2: IIFE (Create New Scope)
for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => {
console.log(j);
}, 1000);
})(i); // pass current i as j
}
// Output: 0, 1, 2Fix 3: Closure via Function Factory
function createPrinter(value) {
return () => console.log(value);
}
for (var i = 0; i < 3; i++) {
setTimeout(createPrinter(i), 1000);
}
// Output: 0, 1, 2Closures and Memory
Closures keep variables alive in memory. This is usually fine, but can cause memory leaks if closures persist longer than needed.
Potential Memory Leak
function createHandler() {
const hugeData = new Array(1000000).fill("x"); // 1M elements
return function () {
console.log(hugeData.length); // closure keeps hugeData alive
};
}
const handler = createHandler();
// hugeData stays in memory as long as `handler` existsFix: Set references to null when done, or restructure to avoid retaining large data.
Interview Deep-Dive Questions
Q1: What will this output?
function createFunctions() {
const result = [];
for (var i = 0; i < 5; i++) {
result.push(function () {
return i;
});
}
return result;
}
const fns = createFunctions();
console.log(fns[0]()); // ?
console.log(fns[2]()); // ?
console.log(fns[4]()); // ?Answer: 5, 5, 5 — all functions close over the same i, which is 5 after the loop.
Q2: Counter with Closure
function counter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
const c = counter();
c.increment();
c.increment();
c.increment();
c.decrement();
console.log(c.getCount()); // ?Answer: 2.
Q3: Can You Explain What a Closure Is in One Sentence?
Best answer: "A closure is a function that has access to its outer function's variables even after the outer function has returned, because the inner function maintains a reference to the outer function's scope."