JavaScript Closures Explained
A closure is a function that remembers the variables from its surrounding scope even after that scope has finished executing. This sounds simple, but it unlocks powerful patterns in JavaScript: data privacy, function factories, event handlers, and much more.
What Is a Closure?
Every function in JavaScript creates a closure. When you define a function, it bundles together the code and the environment where it was created. That environment includes any local variables that were in scope at the time.
Here’s the simplest closure:
function createGreeting(name) {
// This inner function forms a closure
return function() {
return "Hello, " + name + "!";
};
}
const greetAlice = createGreeting("Alice");
const greetBob = createGreeting("Bob");
console.log(greetAlice()); // "Hello, Alice!"
console.log(greetBob()); // "Hello, Bob!"
The inner function still has access to name even after createGreeting has finished running. Each call to createGreeting creates a separate closure with its own name variable.
How Closures Capture Variables
A closure doesn’t just capture values — it captures variables by reference. This distinction matters when those variables change:
function createCounter() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
The closure holds a reference to count, not a copy of its value at creation time. When you call increment(), the closure sees the current value.
Practical Use Cases
Data Privacy
Closures let you create private variables that cannot be accessed from outside:
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit: function(amount) {
if (amount <= 0) {
throw new Error("Deposit amount must be positive");
}
balance += amount;
return balance;
},
withdraw: function(amount) {
if (amount > balance) {
throw new Error("Insufficient funds");
}
balance -= amount;
return balance;
},
getBalance: function() {
return balance;
}
};
}
const account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
// balance is not accessible directly — it is private
No amount of tinkering can access or modify balance directly. The only way in or out is through the methods.
Function Factories
Closures excel at generating specialized functions:
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const square = createMultiplier(x => x * x);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(square(5)); // 25
Each function created by createMultiplier has its own factor value baked in.
Event Handlers
When you attach event handlers in a loop, closures solve the classic “index capture” problem:
// Without closures — this has a bug
for (var i = 0; i < 3; i++) {
document.getElementById('button-' + i).addEventListener('click', function() {
console.log(i); // Always logs 3
});
}
// With closures — fixed using an IIFE
for (var i = 0; i < 3; i++) {
(function(index) {
document.getElementById('button-' + index).addEventListener('click', function() {
console.log(index); // Logs 0, 1, or 2 correctly
});
})(i);
}
// Modern approach — let creates block scope
for (let i = 0; i < 3; i++) {
document.getElementById('button-' + i).addEventListener('click', function() {
console.log(i); // Works correctly due to let
});
}
Common Pitfalls
The Loop Problem
Closures in loops can trip you up if you use var:
// Bug: all functions share the same i
var functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() { return i; });
}
console.log(functions[0]()); // 3 (not 0!)
// Fix: use let or create a new scope
var functions = [];
for (var i = 0; i < 3; i++) {
functions.push((function(index) {
return function() { return index; };
})(i));
}
console.log(functions[0]()); // 0
Simply using let instead of var fixes this problem, because let creates block scope for each iteration.
Memory Concerns
Closures hold references to their outer variables. If you create many closures that reference large objects, you might inadvertently prevent garbage collection:
function createHandler(largeData) {
return function(event) {
console.log(event.target);
// largeData stays in memory as long as this closure exists
};
}
// If you no longer need the handler, nullify references
const handler = createHandler(hugeObject);
element.addEventListener('click', handler);
// Later...
element.removeEventListener('click', handler);
handler = null;
Closures and Performance
Creating closures is not free — each closure allocates memory. However, in most applications, the overhead is negligible. The benefits of cleaner code and data privacy far outweigh the minimal cost.
Modern JavaScript engines optimize closures heavily. A closure that only reads variables (not writes) is especially cheap.
See Also
- Functions and Scope in JavaScript — Learn how scope works and builds the foundation for closures
- Callbacks, Promises, and async/await — See how closures power asynchronous JavaScript
- JavaScript Classes — Understand how classes use closures internally for method binding