JavaScript Closures: Complete Guide
🔐 What is a Closure?
A closure is a function that has access to variables from its outer (enclosing) scope even after the outer function has finished executing. It "closes over" the variables from its lexical environment.
Simple Definition:
A closure gives you access to an outer function's scope from an inner function.
🏗️ Basic Closure Example
function outerFunction(x) {
// Outer function's variable
const outerVariable = x;
// Inner function (closure)
function innerFunction(y) {
// Can access outerVariable even after outerFunction returns
return outerVariable + y;
}
return innerFunction;
}
const addFive = outerFunction(5);
console.log(addFive(10)); // 15
// outerFunction has finished executing, but innerFunction
// still has access to outerVariable (5)
What happens:
outerFunction(5)executes and returnsinnerFunctionouterFunctionfinishes executing and is removed from call stackaddFivenow holds reference toinnerFunction- When
addFive(10)is called, it still remembersouterVariable = 5
🔍 How Closures Work (Under the Hood)
Lexical Scoping:
function outer() {
const message = "Hello";
function inner() {
console.log(message); // Can access 'message' from outer scope
}
return inner;
}
const myClosure = outer();
myClosure(); // "Hello" - still has access to 'message'
Memory Management:
function createCounter() {
let count = 0; // This variable is "trapped" in closure
return function() {
count++; // Closure keeps 'count' alive
return count;
};
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (separate closure, separate count)
console.log(counter1()); // 3
Key Points:
- Each call to
createCounter()creates a new closure - Each closure has its own copy of the
countvariable - The
countvariable stays in memory as long as the closure exists
📚 Practical Closure Examples
1. Data Privacy (Encapsulation)
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return balance;
}
throw new Error("Amount must be positive");
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return balance;
}
throw new Error("Invalid withdrawal amount");
},
getBalance() {
return balance; // Only way to access balance
}
};
}
const account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
// Can't directly access balance
console.log(account.balance); // undefined
2. Function Factories
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenTimes = createMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenTimes(5)); // 50
3. Module Pattern
const TodoModule = (function() {
let todos = []; // Private array
let nextId = 1; // Private counter
return {
add(text) {
const todo = {
id: nextId++,
text: text,
completed: false
};
todos.push(todo);
return todo;
},
remove(id) {
todos = todos.filter(todo => todo.id !== id);
},
toggle(id) {
const todo = todos.find(todo => todo.id === id);
if (todo) {
todo.completed = !todo.completed;
}
},
getAll() {
return [...todos]; // Return copy to prevent mutation
},
getCount() {
return todos.length;
}
};
})();
TodoModule.add("Learn JavaScript");
TodoModule.add("Master Closures");
console.log(TodoModule.getAll());
console.log(TodoModule.getCount()); // 2
4. Event Handlers with State
function createButtonHandler(name) {
let clickCount = 0;
return function() {
clickCount++;
console.log(`${name} clicked ${clickCount} times`);
};
}
// HTML: <button id="btn1">Button 1</button>
// HTML: <button id="btn2">Button 2</button>
document.getElementById('btn1').onclick = createButtonHandler('Button 1');
document.getElementById('btn2').onclick = createButtonHandler('Button 2');
// Each button maintains its own click count
⚠️ Common Closure Pitfalls
1. Loop Problem (Classic Issue)
// ❌ PROBLEM: All functions log 3
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Always logs 3 (the final value of i)
}, 1000);
}
// ✅ SOLUTION 1: Use let (block scope)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Logs 0, 1, 2
}, 1000);
}
// ✅ SOLUTION 2: Use closure with IIFE
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(function() {
console.log(index); // Logs 0, 1, 2
}, 1000);
})(i);
}
// ✅ SOLUTION 3: Use closure function
function createLogger(index) {
return function() {
console.log(index);
};
}
for (var i = 0; i < 3; i++) {
setTimeout(createLogger(i), 1000);
}
2. Memory Leaks
// ❌ PROBLEM: Potential memory leak
function attachListeners() {
const largeData = new Array(1000000).fill('data');
document.getElementById('button').onclick = function() {
// This closure keeps largeData in memory even if we don't use it
console.log('Button clicked');
};
}
// ✅ SOLUTION: Don't reference unnecessary variables
function attachListeners() {
const largeData = new Array(1000000).fill('data');
// Process data if needed
const processedData = largeData.length; // Use only what you need
document.getElementById('button').onclick = function() {
console.log(`Button clicked, data size was: ${processedData}`);
// largeData is not referenced, can be garbage collected
};
}
🎯 Advanced Closure Patterns
1. Currying
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried(...args, ...nextArgs);
};
}
};
}
function add(a, b, c) {
return 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
2. Memoization
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log('Cache hit!');
return cache[key];
}
console.log('Computing...');
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = memoize(fibonacci);
console.log(memoizedFib(10)); // Computing... 55
console.log(memoizedFib(10)); // Cache hit! 55
3. Throttling/Debouncing
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}
// Usage
const debouncedSearch = debounce(function(query) {
console.log(`Searching for: ${query}`);
}, 300);
const throttledScroll = throttle(function() {
console.log('Scrolling...');
}, 100);
🔧 Closure vs Other Concepts
Closure vs Regular Function:
// Regular function - no closure
function regularFunction() {
const data = "I'm temporary";
return data;
}
console.log(regularFunction()); // "I'm temporary"
// 'data' is destroyed after function execution
// Closure function
function closureFunction() {
const data = "I persist!";
return function() {
return data; // Closure keeps 'data' alive
};
}
const closure = closureFunction();
console.log(closure()); // "I persist!" - data is still accessible
Closure vs Class:
// Using Closure
function createPersonClosure(name, age) {
return {
getName() { return name; },
getAge() { return age; },
setAge(newAge) { age = newAge; },
greet() { return `Hi, I'm ${name}, ${age} years old`; }
};
}
// Using Class
class PersonClass {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() { return this.name; }
getAge() { return this.age; }
setAge(newAge) { this.age = newAge; }
greet() { return `Hi, I'm ${this.name}, ${this.age} years old`; }
}
const personClosure = createPersonClosure("Alice", 25);
const personClass = new PersonClass("Bob", 30);
console.log(personClosure.greet()); // "Hi, I'm Alice, 25 years old"
console.log(personClass.greet()); // "Hi, I'm Bob, 30 years old"
📊 Closure Use Cases Summary
| Use Case | Benefit | Example |
|---|---|---|
| Data Privacy | Encapsulation, hide internal state | Bank account, user session |
| Function Factories | Create specialized functions | Multipliers, validators |
| Module Pattern | Organize code, namespace | Libraries, APIs |
| Event Handlers | Maintain state between events | Click counters, form handlers |
| Callbacks | Preserve context | Async operations, timers |
| Memoization | Cache expensive computations | Fibonacci, API responses |
| Currying | Partial function application | Utility functions |
| Debouncing/Throttling | Control function execution rate | Search, scroll handlers |
🎯 Best Practices
1. Use Closures for Data Privacy
// ✅ Good: Private variables
function createConfig() {
let settings = { theme: 'dark', lang: 'en' };
return {
get(key) { return settings[key]; },
set(key, value) { settings[key] = value; }
};
}
2. Be Mindful of Memory Usage
// ✅ Good: Only capture what you need
function createHandler(id) {
return function() {
console.log(`Handler for ${id}`);
};
}
// ❌ Avoid: Capturing unnecessary large objects
function createHandler(user) {
const largeUserData = user.getAllData(); // Large object
return function() {
console.log(`Handler for ${user.id}`); // Only need id
};
}
3. Use Modern Syntax When Appropriate
// Modern arrow function closure
const createMultiplier = (factor) => (number) => number * factor;
const double = createMultiplier(2);
console.log(double(5)); // 10
Closures are a fundamental JavaScript concept that enables powerful patterns like data privacy, function factories, and module systems. Understanding closures is essential for writing effective JavaScript code and understanding how many JavaScript features work under the hood.