JavaScript Core
Closures
Functions that remember their lexical scope
Intermediate
javascript scope functions memory
Definition
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In JavaScript, closures give you access to an outer function’s scope from an inner function, even after the outer function has returned.
The Basics
Simple Closure Example
function outer() {
let count = 0; // Variable in outer scope
function inner() {
count++; // Inner function accesses outer variable
return count;
}
return inner;
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count persists between calls but is not accessible directly
What’s Happening?
outer()creates a local variablecountinner()is defined insideouter(), capturing the lexical scopeouter()returnsinner, which still has access tocount- Each call to
counter()accesses and modifies the samecount
Data Privacy with Closures
Module Pattern
const bankAccount = (function() {
let balance = 0; // Private variable
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return balance;
}
throw new Error('Invalid deposit amount');
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return balance;
}
throw new Error('Insufficient funds');
},
getBalance() {
return balance;
}
};
})();
bankAccount.deposit(100);
console.log(bankAccount.getBalance()); // 100
console.log(bankAccount.balance); // undefined - truly private
Factory Functions
function createUser(name) {
// Private variables
let _password = null;
let _loginAttempts = 0;
return {
name,
setPassword(password) {
if (password.length < 8) {
throw new Error('Password too short');
}
_password = password;
},
login(password) {
_loginAttempts++;
if (password === _password) {
_loginAttempts = 0;
return { success: true, user: this.name };
}
if (_loginAttempts >= 3) {
throw new Error('Account locked');
}
return { success: false, attempts: _loginAttempts };
}
};
}
const user = createUser('John');
user.setPassword('secret123');
console.log(user.login('secret123')); // { success: true, user: 'John' }
Practical Use Cases
Event Handlers with Data
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
// Closure captures 'message'
button.addEventListener('click', function() {
console.log(message);
});
}
setupButton('btn1', 'Button 1 clicked!');
setupButton('btn2', 'Button 2 clicked!');
// Each handler remembers its own message
Debounce Function
function debounce(func, wait) {
let timeoutId; // Persistent across calls
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, wait);
};
}
const handleResize = debounce(() => {
console.log('Resized!');
}, 250);
window.addEventListener('resize', handleResize);
Function Memoization
function memoize(fn) {
const cache = new Map(); // Private cache
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit:', key);
return cache.get(key);
}
console.log('Cache miss:', key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const fibonacci = memoize(function(n) {
if (n < 2) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(40)); // Fast after first call
Closures in Loops (Classic Pitfall)
The Problem
// ❌ Wrong - All buttons alert "Button 5"
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log('Button', i);
}, 100);
}
// Output: "Button 5" (5 times)
Solutions
// Solution 1: Use let (block-scoped)
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log('Button', i);
}, 100);
}
// Output: "Button 0", "Button 1", etc.
// Solution 2: IIFE (pre-ES6)
for (var i = 0; i < 5; i++) {
(function(capturedI) {
setTimeout(() => {
console.log('Button', capturedI);
}, 100);
})(i);
}
// Solution 3: forEach (functional approach)
[0, 1, 2, 3, 4].forEach(i => {
setTimeout(() => {
console.log('Button', i);
}, 100);
});
Advanced Patterns
Partial Application
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
const multiply = (a, b) => a * b;
const double = partial(multiply, 2);
const triple = partial(multiply, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Curry Function
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
const sum = curry((a, b, c) => a + b + c);
console.log(sum(1)(2)(3)); // 6
console.log(sum(1, 2)(3)); // 6
console.log(sum(1)(2, 3)); // 6
Once Function
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const initialize = once(() => {
console.log('Initializing...');
return { status: 'ready' };
});
initialize(); // "Initializing..."
initialize(); // No output, returns cached result
initialize(); // No output, returns cached result
Memory Considerations
Accidental Memory Leaks
// ❌ Potential memory leak
function processLargeData() {
const hugeArray = new Array(1000000).fill('data');
return function() {
console.log('Processing...');
// hugeArray is kept in memory even if not used
};
}
// ✓ Clean closure
function processLargeData() {
const hugeArray = new Array(1000000).fill('data');
const result = process(hugeArray);
return function() {
console.log(result); // Only keeps result, not hugeArray
};
}
Managing Closure Scope
function createDataProcessor() {
let cache = new Map();
return {
process(data) {
if (cache.has(data.id)) {
return cache.get(data.id);
}
const result = expensiveOperation(data);
cache.set(data.id, result);
return result;
},
clearCache() {
cache.clear(); // Manual cleanup
}
};
}
Closures in Modern JavaScript
React Hooks
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// Closure captures 'count'
const timer = setInterval(() => {
console.log(count); // May be stale!
}, 1000);
return () => clearInterval(timer);
}, [count]); // Dependency array
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
ES6 Classes with Private Fields
class Counter {
#count = 0; // True private field (no closure needed)
increment() {
this.#count++;
return this.#count;
}
}
// Under the hood, this still uses closure-like mechanisms
Key Takeaway
Closures are fundamental to JavaScript, enabling data privacy, stateful functions, and powerful patterns like debouncing and memoization. Master them by understanding lexical scope, avoiding common loop pitfalls, and being mindful of memory implications. They’re the foundation for modern patterns from React hooks to module systems.