Understanding JavaScript Closures (Without the Confusion)
Closures are one of those concepts that sound scarier than they are. Let’s demystify them with examples you’ll actually use.
The One-Sentence Explanation
A closure is when a function remembers variables from its outer scope, even after that outer scope has finished executing.
That’s it. Everything else is details.
Example 1: The Classic Counter
function createCounter() {
let count = 0; // Private variable
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
What’s happening:
createCountercreates a variablecount- Returns a function that uses
count - Even after
createCounterfinishes, the inner function still has access tocount - That’s a closure!
Example 2: Private Variables
Closures let you create truly private data:
function createUser(name) {
let password = 'secret123'; // Private!
return {
getName() {
return name;
},
checkPassword(attempt) {
return attempt === password;
},
// No way to directly access password
};
}
const user = createUser('Max');
console.log(user.getName()); // 'Max'
console.log(user.checkPassword('123')); // false
console.log(user.password); // undefined - can't access!
No one can see password from outside. It’s truly private.
Example 3: Event Handlers
This is where closures shine:
function setupButtons() {
const buttons = ['Login', 'Logout', 'Settings'];
buttons.forEach((label, index) => {
const button = document.createElement('button');
button.textContent = label;
// This function closes over 'label' and 'index'
button.addEventListener('click', () => {
console.log(`Button ${label} (${index}) clicked`);
});
document.body.appendChild(button);
});
}
Each click handler remembers its specific label and index.
The Common Mistake: Loop Closures
This doesn’t work as expected:
// ❌ Wrong - all click handlers log 3
for (var i = 0; i < 3; i++) {
const button = document.createElement('button');
button.textContent = i;
button.addEventListener('click', () => {
console.log(i); // Always logs 3!
});
}
Why? All handlers share the same i, which ends up as 3.
Fix 1: Use let
// ✅ Right - let creates new scope each iteration
for (let i = 0; i < 3; i++) {
const button = document.createElement('button');
button.textContent = i;
button.addEventListener('click', () => {
console.log(i); // Logs correct value!
});
}
Fix 2: IIFE (Old school)
for (var i = 0; i < 3; i++) {
(function(index) {
const button = document.createElement('button');
button.textContent = index;
button.addEventListener('click', () => {
console.log(index);
});
})(i); // Pass i as argument
}
But really, just use let. It’s 2025.
Example 4: Module Pattern
Create modules with private state:
const ShoppingCart = (function() {
// Private state
let items = [];
// Private helper
function calculateTotal() {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Public API
return {
add(item) {
items.push(item);
},
remove(itemId) {
items = items.filter(item => item.id !== itemId);
},
getTotal() {
return calculateTotal();
},
getItems() {
return [...items]; // Return copy, not reference
}
};
})();
ShoppingCart.add({ id: 1, price: 10 });
console.log(ShoppingCart.getTotal()); // 10
console.log(ShoppingCart.items); // undefined - private!
Clean API, no global pollution.
Example 5: Function Factory
Create specialized functions:
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Each function remembers its factor.
Example 6: Partial Application
Pre-fill function arguments:
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
const sayHello = partial(greet, 'Hello');
const sayHi = partial(greet, 'Hi');
console.log(sayHello('Max')); // 'Hello, Max!'
console.log(sayHi('Alex')); // 'Hi, Alex!'
React Example: Custom Hooks
Closures are everywhere in React:
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
// These functions close over 'count' and 'setCount'
const increment = useCallback(() => {
setCount(c => c + 1);
}, []);
const decrement = useCallback(() => {
setCount(c => c - 1);
}, []);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return { count, increment, decrement, reset };
}
Memory Considerations
Closures keep variables alive:
function createHugeArray() {
const huge = new Array(1000000).fill('data');
return function() {
return huge.length; // Whole array stays in memory!
};
}
Solution: Only close over what you need:
function createHugeArray() {
const huge = new Array(1000000).fill('data');
const length = huge.length; // Cache what you need
return function() {
return length; // Only closes over 'length'
};
// 'huge' can be garbage collected
}
When to Use Closures
✅ Good use cases:
- Private variables
- Event handlers
- Callbacks
- Factory functions
- Partial application
- Module pattern
❌ When not to use:
- When you don’t need private state
- When simpler alternatives exist
- When memory is tight (mobile)
The Mental Model
Think of closures as a backpack:
function outer() {
const item = 'lunch'; // Put item in backpack
function inner() {
console.log(item); // Still have backpack
}
return inner;
}
const fn = outer(); // Take backpack with you
fn(); // Open backpack, use item
The inner function carries its environment (backpack) wherever it goes.
Debugging Closures
Chrome DevTools shows closure scope:
function createCounter() {
let count = 0;
return function() {
debugger; // Pause here
count++;
return count;
};
}
const counter = createCounter();
counter(); // Check "Closure" in DevTools
You’ll see count in the Closure scope!
Common Interview Questions
Q: What’s a closure? A: A function that has access to variables from its outer scope, even after that scope has executed.
Q: Why are closures useful? A: Data privacy, event handlers, callbacks, factory functions.
Q: What’s the difference between closure and scope? A: Scope is rules for variable access. Closure is when a function remembers its scope.
Conclusion
Closures aren’t magic. They’re just:
- Functions can access outer variables
- Functions can be returned/passed around
- Functions remember their environment
That’s it. Everything else builds on these basics.
Practice by creating:
- A counter (private state)
- Event handlers (remember data)
- A module (private functions)
Once you’ve built these, closures will click.
Now go close over something! 🎯