What Is a Closure?
A closure forms when an inner function references a variable from its outer function's scope, and that inner function survives (is returned, stored, or passed) beyond the outer function's execution.
function makeCounter() {
let count = 0; // This variable "lives on" in the closure
return function() {
count++; // Accesses the outer 'count'
return count;
};
}
const counter = makeCounter(); // makeCounter's execution is DONE
// But 'count' is still alive because 'counter' closes over it
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// Each call to makeCounter creates a NEW closure with its own count
const counter2 = makeCounter();
console.log(counter2()); // 1 β independent from counter
Every function in JavaScript carries a hidden reference to its lexical environment β the scope where it was created. When the function is called later (even after the outer function has returned), it still has access to that environment. This is a closure.
Data Privacy and Encapsulation
Closures let you create "private" variables that cannot be accessed directly from outside β only through the returned interface.
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private β inaccessible from outside
return {
deposit(amount) {
if (amount <= 0) throw new Error("Amount must be positive");
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.deposit(50)); // 150
console.log(account.withdraw(30)); // 120
console.log(account.getBalance()); // 120
// Cannot access balance directly
console.log(account.balance); // undefined β it's private!
Factory Functions with Closures
function createLogger(prefix) {
let logCount = 0;
return {
log(message) {
logCount++;
console.log(`[${prefix}] #${logCount}: ${message}`);
},
getCount() { return logCount; }
};
}
const appLogger = createLogger("APP");
const authLogger = createLogger("AUTH");
appLogger.log("Server started");
appLogger.log("Listening on port 3000");
authLogger.log("User logged in");
authLogger.log("Session created");
appLogger.log("Request received");
console.log("App logs:", appLogger.getCount()); // 3
console.log("Auth logs:", authLogger.getCount()); // 2
Module Pattern
The module pattern uses an IIFE + closure to create a module with private state and a public API β the precursor to ES6 modules.
const cartModule = (function() {
// Private state
const items = [];
let total = 0;
// Private function
function recalculate() {
total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
// Public API
return {
addItem(item) {
items.push(item);
recalculate();
},
removeItem(name) {
const idx = items.findIndex(i => i.name === name);
if (idx !== -1) { items.splice(idx, 1); recalculate(); }
},
getTotal() { return total; },
getItems() { return [...items]; } // Return copy, not reference
};
})();
cartModule.addItem({ name: "Book", price: 15, qty: 2 });
cartModule.addItem({ name: "Pen", price: 3, qty: 5 });
console.log("Total: $" + cartModule.getTotal()); // Total: $45
Memoization with Closures
Memoization caches function results β the cache is stored in a closure variable.
function memoize(fn) {
const cache = {}; // Closed over by the returned function
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log("Cache hit for:", key);
return cache[key];
}
console.log("Computing for:", key);
cache[key] = fn(...args);
return cache[key];
};
}
const slowSquare = memoize(n => {
// Simulate slow computation
return n * n;
});
console.log(slowSquare(5)); // Computing, then 25
console.log(slowSquare(5)); // Cache hit, then 25
console.log(slowSquare(6)); // Computing, then 36
Partial Application
function multiply(a, b) {
return a * b;
}
// Partial application β fix the first argument
function partial(fn, firstArg) {
return function(secondArg) {
return fn(firstArg, secondArg); // firstArg is closed over
};
}
const double = partial(multiply, 2);
const triple = partial(multiply, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(double(10)); // 20
Event Handlers with State
// Simulating a button click tracker (browser pattern)
function createClickTracker(buttonId) {
let clicks = 0;
function handleClick() {
clicks++;
console.log(`Button ${buttonId} clicked ${clicks} time(s)`);
}
// In a browser: document.getElementById(buttonId).addEventListener('click', handleClick);
// Here we simulate:
return handleClick;
}
const btn1Handler = createClickTracker("btn-1");
const btn2Handler = createClickTracker("btn-2");
btn1Handler(); // Button btn-1 clicked 1 time(s)
btn1Handler(); // Button btn-1 clicked 2 time(s)
btn2Handler(); // Button btn-2 clicked 1 time(s) β independent
The Classic Loop Closure Bug
// BUG: var creates one shared 'i' across all iterations
const printFuncsVar = [];
for (var i = 0; i < 3; i++) {
printFuncsVar.push(() => console.log("var i:", i));
}
printFuncsVar[0](); // var i: 3 β not 0!
printFuncsVar[1](); // var i: 3
printFuncsVar[2](); // var i: 3
// FIX 1: Use let β each iteration gets a NEW binding
const printFuncsLet = [];
for (let j = 0; j < 3; j++) {
printFuncsLet.push(() => console.log("let j:", j));
}
printFuncsLet[0](); // let j: 0
printFuncsLet[1](); // let j: 1
printFuncsLet[2](); // let j: 2
// FIX 2: IIFE to capture the value (older workaround)
const printFuncsIIFE = [];
for (var k = 0; k < 3; k++) {
printFuncsIIFE.push((function(captured) {
return () => console.log("IIFE k:", captured);
})(k));
}
printFuncsIIFE[0](); // IIFE k: 0
Memory Considerations
Closures keep the outer scope's variables alive as long as the closure exists. This is intentional but can cause memory leaks if closures are not properly released.
If a closure captures a reference to a large object (e.g., a DOM node or a big array), that object stays in memory as long as the closure exists. Set variables to null when done, or remove event listeners to allow garbage collection.
ποΈ Practical Exercise
Create a createRateLimiter(maxCalls, windowMs) function using a closure that:
- Tracks how many times the returned function has been called within the time window
- Allows the call if under the limit
- Blocks with "Rate limit exceeded" otherwise
- Resets the counter after
windowMsmilliseconds
The counter must be private (closure variable).
π₯ Challenge Exercise
Implement an observable(initialValue) function using closures that:
- Stores a private value
- Returns an object with
get(),set(newVal), andsubscribe(fn) - When
setis called, notifies all subscribed functions with(newVal, oldVal) - Returns an unsubscribe function from
subscribe
π Summary
- A closure = a function + its lexical environment (the variables from its defining scope).
- Inner functions close over outer variables, keeping them alive even after the outer function returns.
- Use closures for: private state, factory functions, module pattern, memoization, partial application.
- The classic var loop bug is fixed by using
let(block-scoped per iteration). - Be mindful of memory: closures prevent garbage collection of captured variables.
Interview Questions
- What is a closure in JavaScript? Provide a definition and example.
- How do closures enable data privacy/encapsulation?
- Explain the classic for-loop closure bug with var and how let fixes it.
- What is the module pattern and how does it use closures?
- Can closures cause memory leaks? How do you prevent them?
Frequently Asked Questions
Technically yes β every JavaScript function closes over its surrounding scope. But we typically say a function "is a closure" when it actually makes use of variables from an outer scope that has already returned, making the concept meaningful and visible.
Both provide private state and a public interface. Classes use prototype-based inheritance and the class keyword. Closures are simpler (functions + variables) and don't require this or new. Closures are better for functional style; classes for object-oriented patterns with inheritance.
Yes. Closures capture the variable binding (reference), not the value. With const, you can't reassign the binding, but if the const holds an object or array, you can still mutate its contents via the closure.