Closures
Closures let inner functions remember variables from their outer scope even after the outer function has returned. This enables powerful patterns like data privacy, memoization, and function factories.
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.
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 returns a function. That function has a closure over count โ it keeps a reference to count and can read and update it, even though makeCounter has already finished running.
How Closures Work
When a function is created, it captures a reference to the scope chain at that point. This isn't a copy of the values โ it's a live reference:
function makeMultiplier(factor) {
// factor is captured in the closure
return (number) => number * factor;
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Each closure has its own independent `factor`
Each call to makeMultiplier creates a new closure with its own factor. double and triple don't share state.
Data Privacy
Closures are JavaScript's way of creating private state โ variables that can't be accessed from outside:
function createBankAccount(initialBalance) {
let balance = initialBalance; // private
return {
deposit(amount) {
if (amount > 0) balance += amount;
return balance;
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return balance;
}
return "Insufficient funds";
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
account.withdraw(30);
console.log(account.getBalance()); // 120
// console.log(account.balance); // undefined โ private!
The Classic Loop Bug
This is one of the most famous closure gotchas:
// โ Bug: using var
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs[0](); // 3 โ not 0!
funcs[1](); // 3 โ not 1!
funcs[2](); // 3 โ not 2!
Because var is function-scoped, all three functions share the same i. By the time they're called, the loop has ended and i === 3.
Fix 1 โ Use let (block-scoped):
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs[0](); // 0 โ
funcs[1](); // 1 โ
funcs[2](); // 2 โ
let creates a new binding for each iteration.
Fix 2 โ IIFE (older pattern):
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push((function(j) {
return function() { console.log(j); };
})(i)); // immediately invoked, capturing i as j
}
Memoization
Closures can cache expensive computation results:
function memoize(fn) {
const cache = {}; // private to this closure
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log("Cache hit!");
return cache[key];
}
cache[key] = fn(...args);
return cache[key];
};
}
function slowSquare(n) {
// imagine this is expensive
return n * n;
}
const fastSquare = memoize(slowSquare);
console.log(fastSquare(10)); // computes: 100
console.log(fastSquare(10)); // Cache hit! 100
console.log(fastSquare(20)); // computes: 400
console.log(fastSquare(20)); // Cache hit! 400
Partial Application
Closures let you "pre-fill" arguments:
function multiply(a, b) {
return a * b;
}
function partial(fn, ...presetArgs) {
return (...laterArgs) => fn(...presetArgs, ...laterArgs);
}
const double = partial(multiply, 2);
const triple = partial(multiply, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(double(12)); // 24
IIFE โ Immediately Invoked Function Expression
An IIFE is a function defined and immediately called. It creates a private scope:
const result = (function() {
const private = "I'm private";
return private.toUpperCase();
})();
console.log(result); // "I'M PRIVATE"
// private is not accessible here
IIFEs were used before ES modules to avoid polluting the global scope. Modern code uses ES modules instead, but you'll still see IIFEs in legacy codebases.
Closure in Event Handlers
Closures are everywhere in browser code:
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
button.addEventListener("click", function() {
// This function closes over `message`
alert(message);
});
}
setupButton("btn1", "Hello from Button 1");
setupButton("btn2", "Hello from Button 2");
// Each button remembers its own message
Key Takeaways
- A closure is a function that retains access to its outer scope after that scope has exited
- Closures create private state โ variables inaccessible from outside
- Each closure gets its own copy of the closed-over variables
- The classic
var+forloop bug is caused by sharing scope โ fix withlet - Practical uses: counters, memoization, partial application, data privacy
Ready to test your knowledge?
Take a quiz on what you just learned.