JavaScript Closures Explained (with Real Examples)
Direct Answer & Key Concept
A closure is the combination of a function bundled together with references to its surrounding lexical environment. Closures give an inner function access to an outer function's scope even after the outer function has finished executing.
// Classic Counter Closure Example
function createCounter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
get: () => count
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log("Current:", counter.get()); // Current: 11What is a closure in JavaScript?
A closure is a core JavaScript mechanism wherein an inner function retains access to variables and parameters declared in its outer (enclosing) function's lexical scope, even after the outer function has completed execution and returned.
In technical terms, in JavaScript, closures are created every time a function is created, at function creation time. Whenever an inner function references identifiers from an enclosing scope, JavaScript preserves that lexical environment on the heap rather than popping it from memory with the call stack.
Why are closures useful in web development?
Closures solve fundamental software engineering challenges in JavaScript by enabling state retention and encapsulation:
1. Data Privacy and Encapsulation
JavaScript lacked native private class fields for decades. Closures provided the standard pattern to hide implementation details and prevent unauthorized external mutation:
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable hidden by closure
return {
deposit(amount) {
if (amount > 0) balance += amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const myAccount = createBankAccount(100);
myAccount.deposit(50);
console.log(myAccount.getBalance()); // 150
console.log(myAccount.balance); // undefined (Private!)
2. Function Factories and Currying
Closures allow you to author factory functions that generate customized functions tailored with preset configuration parameters:
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
How do closures interact with the Event Loop?
A classic JavaScript interview puzzle demonstrates how closures interact with asynchronous execution and variable scoping:
// Problematic code with var:
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 4, 4, 4 (because var is function-scoped and shared)
// Solved with block-scoped let:
for (let j = 1; j <= 3; j++) {
setTimeout(() => console.log(j), 100);
}
// Prints: 1, 2, 3 (each iteration creates a new lexical closure binding)
When using let, JavaScript creates a new lexical scope binding for each loop iteration, ensuring each timer callback closes over its own distinct value of j.
Common Mistakes with JavaScript Closures
- Unintentional memory retention (Memory Leaks): Retaining closures inside global caches, event listeners, or long-lived intervals prevents the garbage collector from freeing memory for referenced variables.
- Confusing closures with object copies: Closures capture references to live variables, not static snapshots of values. Mutating an enclosed variable alters the value for all functions sharing that closure.
- Overusing closures where pure functions suffice: Creating unnecessary closures in high-frequency loops creates extra function allocations and memory pressure.