JavaScript Closures Explained Simply
Understand JavaScript closures with clear examples. Learn how closures work, why they matter, and how to use them in real code patterns like debounce and private variables.
JavaScript Closures Explained Simply
Closures are one of the most important concepts in JavaScript. They sound intimidating, but they're actually something you're already using — you just might not know it yet.
What is a Closure?
A closure is when a function "remembers" variables from its outer scope, even after that outer function has finished executing.
function createGreeter(greeting) {
return function(name) {
console.log(`${greeting}, ${name}!`);
};
}
const sayHello = createGreeter("Hello");
sayHello("Priya"); // "Hello, Priya!"
sayHello("Rahul"); // "Hello, Rahul!"
createGreeter has already returned. It's done. But the inner function still has access to greeting. That's a closure.
How Closures Work
JavaScript has lexical scoping — a function looks up variables based on where it was defined, not where it's called.
When a function is created, it captures a reference to its surrounding scope. This captured scope stays alive as long as the function exists.
function counter() {
let count = 0;
return {
increment() { count++; },
getCount() { return count; }
};
}
const myCounter = counter();
myCounter.increment();
myCounter.increment();
console.log(myCounter.getCount()); // 2
The variable count lives inside the closure. Nothing outside can access it directly — it's effectively private.
Why Closures Matter
1. Data Privacy
Closures let you create private variables in JavaScript — something the language doesn't natively support (without classes):
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.withdraw(200); // 1300
// account.balance — undefined! Can't access directly
2. Function Factories
Create specialized functions from a general template:
function multiply(factor) {
return function(number) {
return number * factor;
};
}
const double = multiply(2);
const triple = multiply(3);
double(5); // 10
triple(5); // 15
3. Callbacks and Event Handlers
Every callback that uses a variable from its enclosing scope is a closure:
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
button.addEventListener("click", function() {
alert(message); // 'message' is captured from outer scope
});
}
setupButton("btn1", "Welcome!");
setupButton("btn2", "Goodbye!");
The Classic Closure Trap
This trips up every JavaScript developer at least once:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Prints: 3, 3, 3 (not 0, 1, 2!)
Why? var is function-scoped, not block-scoped. By the time the timeouts fire, i is already 3.
Fix with let:
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Prints: 0, 1, 2 ✓
let creates a new binding for each iteration — each callback gets its own i.
Fix with an IIFE (pre-ES6):
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
Real-World Closure Patterns
Debounce
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
timeoutId is captured in the closure — it persists across calls.
Memoization
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key] !== undefined) return cache[key];
cache[key] = fn.apply(this, args);
return cache[key];
};
}
The cache object lives in the closure — it remembers previous results.
Once Function
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
Closures and Memory
Closures keep their outer scope alive, which means those variables stay in memory. This usually isn't a problem, but in rare cases (like storing closures in long-lived data structures), it can cause memory leaks.
Tip: If you're done with a closure, set the reference to null so the garbage collector can clean up.
Key Takeaways
- A closure is a function that remembers its outer scope
- Closures enable private variables, function factories, and callbacks
- Use
letinstead ofvarin loops to avoid the classic trap - Debounce, throttle, and memoize all rely on closures
- Closures are everywhere in JavaScript — React hooks, event handlers, callbacks
🚀 Practice What You Learned
Apply these concepts with hands-on coding challenges: