Master one of JavaScript's most powerful features: closures. Learn how they work, when to use them, and how to avoid common pitfalls. 🚀
Understanding Closures 🎯
A closure is the combination of a function and the lexical environment within which that function was declared.
function createCounter() {
let count = 0; // Private variable
return {
increment() {
return ++count;
},
decrement() {
return --count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
console.log(counter.decrement()); // 1
Practical Applications 💡
1. Data Privacy
function createWallet(initialBalance = 0) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return `Deposited ${amount}. New balance: ${balance}`;
}
throw new Error('Invalid deposit amount');
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return `Withdrawn ${amount}. New balance: ${balance}`;
}
throw new Error('Invalid withdrawal amount');
},
getBalance() {
return balance;
}
};
}
const wallet = createWallet(100);
console.log(wallet.getBalance()); // 100
console.log(wallet.deposit(50)); // Deposited 50. New balance: 150
console.log(wallet.withdraw(30)); // Withdrawn 30. New balance: 120
2. Function Factories
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
3. Memoization
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Fetching from cache');
return cache.get(key);
}
console.log('Calculating result');
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveFunction = (n) => {
console.log('Computing...');
return n * 2;
};
const memoizedFunction = memoize(expensiveFunction);
console.log(memoizedFunction(5)); // Computing... 10
console.log(memoizedFunction(5)); // Fetching from cache 10
Advanced Patterns 🔧
1. Module Pattern
const ShoppingCart = (function() {
const items = new Map();
function addItem(id, name, price, quantity = 1) {
if (items.has(id)) {
const existingItem = items.get(id);
existingItem.quantity += quantity;
} else {
items.set(id, { name, price, quantity });
}
}
function removeItem(id, quantity = 1) {
if (items.has(id)) {
const item = items.get(id);
item.quantity = Math.max(0, item.quantity - quantity);
if (item.quantity === 0) {
items.delete(id);
}
}
}
function getTotal() {
let total = 0;
for (const item of items.values()) {
total += item.price * item.quantity;
}
return total;
}
return {
addItem,
removeItem,
getTotal,
getItems: () => Array.from(items.entries())
};
})();
ShoppingCart.addItem(1, "Book", 29.99, 2);
ShoppingCart.addItem(2, "Pen", 4.99, 3);
console.log(ShoppingCart.getTotal()); // 74.95
2. Currying
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6
3. Event Emitter
function createEventEmitter() {
const events = new Map();
return {
on(event, callback) {
if (!events.has(event)) {
events.set(event, new Set());
}
events.get(event).add(callback);
return () => {
events.get(event).delete(callback);
if (events.get(event).size === 0) {
events.delete(event);
}
};
},
emit(event, ...args) {
if (events.has(event)) {
for (const callback of events.get(event)) {
callback.apply(null, args);
}
}
},
off(event, callback) {
if (events.has(event)) {
events.get(event).delete(callback);
if (events.get(event).size === 0) {
events.delete(event);
}
}
}
};
}
const emitter = createEventEmitter();
const unsubscribe = emitter.on('userLoggedIn', (user) => {
console.log(`${user} logged in`);
});
emitter.emit('userLoggedIn', 'John'); // John logged in
unsubscribe(); // Remove listener
Common Pitfalls and Solutions 🚨
1. Memory Leaks
// Bad Practice ❌
function createButtons() {
let count = 0;
const buttons = [];
for (let i = 0; i < 10; i++) {
const button = document.createElement('button');
button.innerHTML = `Button ${i}`;
// This creates a closure that holds reference to count
button.addEventListener('click', function() {
console.log(count++);
});
buttons.push(button);
}
return buttons;
}
// Good Practice ✅
function createButtons() {
const buttons = [];
for (let i = 0; i < 10; i++) {
const button = document.createElement('button');
button.innerHTML = `Button ${i}`;
// Use data attributes instead
button.dataset.count = '0';
button.addEventListener('click', function() {
const currentCount = parseInt(this.dataset.count);
this.dataset.count = currentCount + 1;
console.log(this.dataset.count);
});
buttons.push(button);
}
return buttons;
}
2. Loop Variables
// Bad Practice ❌
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// Prints: 3, 3, 3
// Good Practice ✅
// Using let creates a new binding for each iteration
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// Prints: 0, 1, 2
// Alternative using IIFE
for (var i = 0; i < 3; i++) {
(function(i) {
setTimeout(() => console.log(i), 1000);
})(i);
}
// Prints: 0, 1, 2
Performance Considerations 🚀
// Avoid creating closures in loops
// Bad Practice ❌
const handlers = [];
for (let i = 0; i < 1000; i++) {
handlers.push(() => {
console.log(i);
});
}
// Good Practice ✅
function createHandler(value) {
return () => {
console.log(value);
};
}
const handlers = [];
for (let i = 0; i < 1000; i++) {
handlers.push(createHandler(i));
}
Best Practices 📝
- Use closures for data privacy
- Avoid unnecessary closures
- Be mindful of memory usage
- Use let instead of var
- Clean up event listeners
- Implement proper error handling
- Use proper documentation
- Follow functional programming principles
- Consider performance implications
- Test closure behavior thoroughly
Additional Resources
Understanding closures is essential for writing clean, maintainable JavaScript code. They provide powerful patterns for data privacy, state management, and functional programming techniques.