What is a Closure?
A closure is a function that "remembers" the variables from its outer scope even after the outer function has returned. It's one of JavaScript's most powerful features — and one of the trickiest to fully grasp.
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
// makeCounter() has returned, but count still exists!
// The inner function closes over (keeps a reference to) count
const counter2 = makeCounter(); // separate closure, separate count
console.log(counter2()); // 1 — independent counter
Practical Closure Patterns
// Factory function — creates customized functions
function multiplier(factor) {
return (number) => number * factor; // closes over 'factor'
}
const double = multiplier(2);
const triple = multiplier(3);
const tenTimes = multiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenTimes(5)); // 50
// Private data via closure
function createBankAccount(initialBalance) {
let balance = initialBalance; // private — not accessible from outside!
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(100);
console.log(account.getBalance()); // 100
account.deposit(50); // 150
account.withdraw(30); // 120
// console.log(account.balance); // undefined — truly private!
// Memoization — cache expensive results
function memoize(fn) {
const cache = {};
return function(n) {
if (n in cache) {
console.log("From cache");
return cache[n];
}
cache[n] = fn(n);
return cache[n];
};
}
const slowSquare = (n) => {
// imagine this is slow...
return n * n;
};
const fastSquare = memoize(slowSquare);
fastSquare(10); // computed
fastSquare(10); // "From cache"
The Classic Closure Gotcha
// Classic bug — var in loops
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 — all see the SAME i (final value)
// Fix 1: use let (block-scoped, each iteration gets own i)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 ✅
// Fix 2: IIFE to create separate scope per iteration (old way)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Prints: 0, 1, 2 ✅
// This is why let was invented — it solves this naturally
⚡ Key Takeaways
- A closure is a function that retains access to its outer scope after the outer function returns
- Closures enable private variables and data encapsulation
- Factory functions use closures to create customized functions
- Memoization uses closures to cache computation results
- Classic bug:
varin loops with async callbacks — fix withlet
🎯 Practice Exercises
EXERCISE 1
Create a makeAdder(x) function that returns a function adding x to its argument. Use it to create add5 and add10 functions.
EXERCISE 2 — CHALLENGE
Implement a rate limiter using closure: rateLimit(fn, limit, window) that only allows fn to be called limit times per window milliseconds.