JavaScript Closures: The Complete Guide
Master JavaScript closures with practical examples. Understand lexical scoping, function factories, private variables, and common closure patterns.
JavaScript Closures: The Complete Guide
Closures are one of the most powerful and often misunderstood features in JavaScript. They enable function factories, private variables, and many advanced patterns that make JavaScript unique.
What is a Closure?
A closure is a function that has access to variables in its outer (enclosing) lexical scope, even after the outer function has returned.
function outerFunction(x) {
// Variable in outer scope
return function innerFunction(y) {
// Inner function has access to x
return x + y;
};
}
const addFive = outerFunction(5);
console.log(addFive(3)); // 8
console.log(addFive(7)); // 12
// The inner function "closes over" the variable x
How Closures Work
Lexical Scoping
JavaScript uses lexical scoping, meaning functions are executed using the variable scope that was in effect when they were defined.
const globalVar = 'global';
function outerFunc() {
const outerVar = 'outer';
function middleFunc() {
const middleVar = 'middle';
function innerFunc() {
const innerVar = 'inner';
// Can access all outer scopes
console.log(globalVar); // 'global'
console.log(outerVar); // 'outer'
console.log(middleVar); // 'middle'
console.log(innerVar); // 'inner'
}
return innerFunc;
}
return middleFunc;
}
const myFunc = outerFunc()();
myFunc();
Closure Creation
// Example 1: Basic closure
function createGreeting(name) {
return function () {
return `Hello, ${name}!`;
};
}
const greetJohn = createGreeting('John');
const greetJane = createGreeting('Jane');
console.log(greetJohn()); // 'Hello, John!'
console.log(greetJane()); // 'Hello, Jane!'
// Example 2: Multiple closures
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
},
};
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1.increment()); // 1
console.log(counter1.increment()); // 2
console.log(counter2.increment()); // 1 (independent)
console.log(counter1.getCount()); // 2
console.log(counter2.getCount()); // 1
Common Use Cases
1. Data Privacy
// Private variables with closures
function createBankAccount(initialBalance) {
let balance = initialBalance;
let transactionHistory = [];
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
transactionHistory.push({
type: 'deposit',
amount,
balance,
timestamp: new Date(),
});
return true;
}
return false;
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
transactionHistory.push({
type: 'withdrawal',
amount,
balance,
timestamp: new Date(),
});
return true;
}
return false;
},
getBalance() {
return balance;
},
getStatement() {
return transactionHistory.map(
(t) => `${t.type}: $${t.amount} (Balance: $${t.balance})`
);
},
};
}
const account = createBankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
console.log(account.balance); // undefined (private)
2. Function Factories
// Creating specialized functions
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
// More complex factory
function createValidator(rules) {
return function (value) {
for (const rule of rules) {
if (!rule.test(value)) {
return { valid: false, error: rule.message };
}
}
return { valid: true };
};
}
const passwordValidator = createValidator([
{
test: (val) => val.length >= 8,
message: 'Password must be at least 8 characters',
},
{
test: (val) => /[A-Z]/.test(val),
message: 'Password must contain uppercase letter',
},
{
test: (val) => /[0-9]/.test(val),
message: 'Password must contain number',
},
]);
console.log(passwordValidator('pass'));
// { valid: false, error: 'Password must be at least 8 characters' }
console.log(passwordValidator('Password1'));
// { valid: true }
3. Event Handlers
// Closure in event handlers
function createButtonHandler(buttonName) {
let clickCount = 0;
return function (event) {
clickCount++;
console.log(`${buttonName} clicked ${clickCount} times`);
};
}
// Each button has its own click counter
document
.getElementById('btn1')
.addEventListener('click', createButtonHandler('Button 1'));
document
.getElementById('btn2')
.addEventListener('click', createButtonHandler('Button 2'));
// Practical example: Dynamic menu
function createMenu(items) {
const menuElement = document.createElement('ul');
items.forEach((item, index) => {
const li = document.createElement('li');
li.textContent = item.name;
// Closure captures item and index
li.addEventListener('click', function () {
console.log(`Clicked: ${item.name} at index ${index}`);
if (item.action) {
item.action();
}
});
menuElement.appendChild(li);
});
return menuElement;
}
4. Module Pattern
// Classic module pattern using closures
const Calculator = (function () {
// Private variables and functions
let result = 0;
let history = [];
function addToHistory(operation, value) {
history.push({
operation,
value,
result,
timestamp: new Date(),
});
}
// Public API
return {
add(value) {
result += value;
addToHistory('add', value);
return this;
},
subtract(value) {
result -= value;
addToHistory('subtract', value);
return this;
},
multiply(value) {
result *= value;
addToHistory('multiply', value);
return this;
},
divide(value) {
if (value !== 0) {
result /= value;
addToHistory('divide', value);
}
return this;
},
getResult() {
return result;
},
clear() {
result = 0;
history = [];
return this;
},
getHistory() {
return history.map((h) => `${h.operation}(${h.value}) = ${h.result}`);
},
};
})();
Calculator.add(10).multiply(2).subtract(5);
console.log(Calculator.getResult()); // 15
console.log(Calculator.getHistory());
Advanced Closure Patterns
Partial Application
// Partial application using closures
function partial(fn, ...presetArgs) {
return function (...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
// Example usage
function greet(greeting, punctuation, name) {
return `${greeting}, ${name}${punctuation}`;
}
const greetHello = partial(greet, 'Hello', '!');
const greetGoodbye = partial(greet, 'Goodbye', '.');
console.log(greetHello('John')); // 'Hello, John!'
console.log(greetGoodbye('Jane')); // 'Goodbye, Jane.'
// Currying
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...nextArgs) {
return curried(...args, ...nextArgs);
};
};
}
const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2)(3)); // 6
console.log(add(1, 2, 3)); // 6
Memoization
// Memoization using closures
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Returning cached result');
return cache.get(key);
}
console.log('Computing result');
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Expensive computation
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = memoize(fibonacci);
console.time('First call');
console.log(memoizedFib(40)); // Slow
console.timeEnd('First call');
console.time('Second call');
console.log(memoizedFib(40)); // Fast (cached)
console.timeEnd('Second call');
// Advanced memoization with TTL
function memoizeWithTTL(fn, ttl = 60000) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value;
}
const result = fn.apply(this, args);
cache.set(key, {
value: result,
timestamp: Date.now(),
});
return result;
};
}
Debouncing and Throttling
// Debounce using closures
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Throttle using closures
function throttle(fn, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage
const expensiveOperation = () => console.log('Expensive operation');
const debouncedOp = debounce(expensiveOperation, 300);
const throttledOp = throttle(expensiveOperation, 300);
// Debounce: Only executes after 300ms of no calls
window.addEventListener('resize', debouncedOp);
// Throttle: Executes at most once every 300ms
window.addEventListener('scroll', throttledOp);
Common Pitfalls
1. Loop Variable Closure
// Problem: All functions refer to same variable
function createFunctions() {
const functions = [];
for (var i = 0; i < 5; i++) {
functions.push(function () {
console.log(i);
});
}
return functions;
}
const funcs = createFunctions();
funcs[0](); // 5 (not 0!)
funcs[1](); // 5 (not 1!)
// Solution 1: Use let (block scope)
function createFunctionsFixed1() {
const functions = [];
for (let i = 0; i < 5; i++) {
functions.push(function () {
console.log(i);
});
}
return functions;
}
// Solution 2: IIFE
function createFunctionsFixed2() {
const functions = [];
for (var i = 0; i < 5; i++) {
functions.push(
(function (index) {
return function () {
console.log(index);
};
})(i)
);
}
return functions;
}
// Solution 3: Factory function
function createFunctionsFixed3() {
const functions = [];
function makeFunction(index) {
return function () {
console.log(index);
};
}
for (var i = 0; i < 5; i++) {
functions.push(makeFunction(i));
}
return functions;
}
2. Memory Leaks
// Potential memory leak
function createLeak() {
const largeData = new Array(1000000).fill('data');
return function () {
// Only uses one element but keeps entire array in memory
return largeData[0];
};
}
// Better approach
function avoidLeak() {
const largeData = new Array(1000000).fill('data');
const firstElement = largeData[0];
return function () {
return firstElement;
};
}
// Managing closures in event listeners
function attachHandler() {
const expensiveData = generateExpensiveData();
function handler(event) {
processData(expensiveData);
}
element.addEventListener('click', handler);
// Return cleanup function
return function cleanup() {
element.removeEventListener('click', handler);
};
}
const cleanup = attachHandler();
// Later: cleanup() to prevent memory leak
3. Performance Considerations
// Avoid creating unnecessary closures
// Bad: Creates new function on every render
function BadComponent({ items }) {
return items.map((item) => (
<button onClick={() => handleClick(item.id)}>{item.name}</button>
));
}
// Good: Create handler once
function GoodComponent({ items }) {
const createHandler = (id) => () => handleClick(id);
const handlers = useMemo(
() => items.map((item) => createHandler(item.id)),
[items]
);
return items.map((item, index) => (
<button onClick={handlers[index]}>{item.name}</button>
));
}
Testing Closures
// Testing private variables
function testCounter() {
const counter = createCounter();
console.assert(counter.getCount() === 0, 'Initial count should be 0');
counter.increment();
console.assert(counter.getCount() === 1, 'Count should be 1 after increment');
counter.decrement();
counter.decrement();
console.assert(
counter.getCount() === -1,
'Count should handle negative values'
);
// Test independence
const counter2 = createCounter();
console.assert(counter2.getCount() === 0, 'New counter should start at 0');
console.assert(
counter.getCount() === -1,
'Original counter should be unchanged'
);
}
// Testing memoization
function testMemoization() {
let callCount = 0;
const expensive = (n) => {
callCount++;
return n * 2;
};
const memoized = memoize(expensive);
console.assert(memoized(5) === 10, 'Should return correct result');
console.assert(callCount === 1, 'Should call function once');
memoized(5);
console.assert(
callCount === 1,
'Should not call function again for same input'
);
memoized(6);
console.assert(callCount === 2, 'Should call function for new input');
}
Best Practices
-
Use closures for data privacy
// Good: Private state function createStore(initialState) { let state = initialState; const listeners = []; return { getState: () => state, setState: (newState) => { state = newState; listeners.forEach((listener) => listener(state)); }, subscribe: (listener) => { listeners.push(listener); return () => { const index = listeners.indexOf(listener); listeners.splice(index, 1); }; }, }; }
-
Avoid unnecessary closures
// Avoid creating functions in loops when not needed // Bad for (let i = 0; i < 1000; i++) { arr[i] = function () { return i * 2; }; } // Good function double(i) { return i * 2; } for (let i = 0; i < 1000; i++) { arr[i] = double; }
-
Clean up closures
function createResource() { const resource = acquireResource(); function cleanup() { releaseResource(resource); } return { use: () => useResource(resource), dispose: cleanup, }; }
-
Document closure behavior
/** * Creates a rate limiter that allows max calls within windowMs * @param {number} max - Maximum number of calls * @param {number} windowMs - Time window in milliseconds * @returns {Function} Rate limited function */ function rateLimit(fn, max, windowMs) { const calls = []; return function (...args) { const now = Date.now(); const recentCalls = calls.filter((time) => now - time < windowMs); if (recentCalls.length < max) { calls.push(now); return fn.apply(this, args); } }; }
Conclusion
Closures are fundamental to JavaScript and enable powerful patterns:
- Data privacy through lexical scoping
- Function factories for creating specialized functions
- State persistence across function calls
- Event handling with preserved context
- Module pattern for encapsulation
Key takeaways:
- Closures "remember" their lexical environment
- Inner functions have access to outer variables
- Be mindful of memory implications
- Use closures to create powerful abstractions
- Understand common pitfalls to avoid bugs
Master closures to write more elegant and maintainable JavaScript code!