JavaScript Closures Explained: How They Work and Why They Matter
Closures are one of the most-asked JavaScript interview topics — and one of the most misunderstood. Once you truly get them, they become a powerful tool you'll use every day.
What Is a Closure?
A closure is a function that remembers the variables from the scope where it was defined, even after that scope has finished executing.
function makeCounter() {
let count = 0; // This variable is "closed over"
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count is not accessible from outside!
console.log(count); // ReferenceError: count is not defined
The inner function "closes over" the count variable. Even after makeCounter() returns, the inner function still has access to count.
How Closures Work: Lexical Scope
JavaScript uses lexical scoping: a function's scope is determined by where it's written in the code, not where it's called.
const name = 'Global';
function outer() {
const name = 'Outer';
function inner() {
// inner() looks up scope chain: inner → outer → global
console.log(name); // 'Outer' (not 'Global')
}
inner();
}
outer();
The scope chain for inner() is:
inner's own scopeouter's scope ← findsnamehere- Global scope
Common Use Cases
1. Data Privacy / Module Pattern
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private — can't be accessed directly
return {
deposit(amount) {
if (amount > 0) 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);
console.log(account.getBalance()); // 1000
account.deposit(500);
console.log(account.getBalance()); // 1500
// Can't access balance directly!
console.log(account.balance); // undefined
2. Factory Functions
function createMultiplier(factor) {
// factor is closed over
return (number) => number * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenX = createMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenX(5)); // 50
3. Memoization (Caching)
function memoize(fn) {
const cache = new Map(); // Closed over by the returned function
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit!');
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = memoize((n) => {
// Simulates expensive computation
let result = 0;
for (let i = 0; i < n * 1000000; i++) result += i;
return result;
});
expensiveCalc(10); // Computes (slow)
expensiveCalc(10); // Cache hit! (instant)
4. Partial Application
function multiply(a, b) {
return a * b;
}
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
const multiplyByFive = partial(multiply, 5);
console.log(multiplyByFive(3)); // 15
console.log(multiplyByFive(10)); // 50
5. Event Handlers with State
function createButton(label) {
let clickCount = 0;
const button = document.createElement('button');
button.textContent = label;
button.addEventListener('click', function() {
// This handler closes over clickCount
clickCount++;
console.log(`"${label}" clicked ${clickCount} times`);
});
return button;
}
const btn1 = createButton('Save'); // Has its own clickCount
const btn2 = createButton('Delete'); // Has its own clickCount
The Classic Loop Bug (and Fix)
// ❌ Bug: all handlers close over the same 'i'
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints 3, 3, 3 — not 0, 1, 2!
}, 100);
}
// By the time timeouts run, the loop has finished and i === 3
// ✅ Fix 1: use let (creates new binding per iteration)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints 0, 1, 2 ✓
}, 100);
}
// ✅ Fix 2: IIFE to capture i
for (var i = 0; i < 3; i++) {
(function(capturedI) {
setTimeout(function() {
console.log(capturedI); // Prints 0, 1, 2 ✓
}, 100);
})(i);
}
Closures in Modern JavaScript
// React hooks use closures heavily
function Counter() {
const [count, setCount] = useState(0);
// handleClick closes over count and setCount
const handleClick = () => {
setCount(count + 1); // uses closed-over count
};
return <button onClick={handleClick}>{count}</button>;
}
// Stale closure issue (classic React bug)
function BadCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// ❌ Stale closure: count is always 0 from first render
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []); // Empty deps — closes over initial count
return <div>{count}</div>;
}
// Fix: use functional update
function GoodCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// ✅ Functional update: always uses latest count
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}
Memory Considerations
// ⚠️ Closures keep outer variables alive
function createHeavyObject() {
const largeData = new Array(1000000).fill('data'); // 1M elements
return function() {
// largeData can never be garbage collected while this function exists!
return largeData[0];
};
}
// ✅ Limit what's closed over
function createHeavyObjectFixed() {
const largeData = new Array(1000000).fill('data');
const firstItem = largeData[0]; // Only capture what you need
return function() {
return firstItem; // largeData can now be GC'd
};
}
Summary
- A closure is a function + its captured scope (lexical environment)
- Closures enable data privacy, factory functions, and stateful callbacks
- Every function in JavaScript is a closure (they all capture their surrounding scope)
- Watch for stale closures in React hooks and async code
- Memory: closures keep referenced variables alive — avoid capturing large objects unnecessarily
→ Obfuscate your JavaScript code with the String Obfuscator tool.