Closures
Understanding closures and lexical scoping in JavaScript
What is a Closure?
A closure is a function that has access to variables from its outer (enclosing) function's scope, even after the outer function has returned. Closures are created every time a function is created in JavaScript.
The Three Scope Chains
A closure has access to:
- 1. Its own scope — Variables defined between its curly braces
- 2. Outer function's variables — Variables from the enclosing function
- 3. Global variables — Variables in the global scope
Lexical Scoping
JavaScript uses lexical scoping, meaning a function's scope is determined by where it's defined in the source code, not where it's called.
function outer() {
const message = "Hello from outer!";
function inner() {
// inner() has access to 'message' due to lexical scoping
console.log(message);
}
return inner;
}
const closureFn = outer();
closureFn(); // "Hello from outer!" - still has access!
Basic Closure Example
Here's a simple closure that creates a counter:
function createCounter() {
let count = 0; // Private variable
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
// count is private - can't access directly
console.log(counter.count); // undefined
Practical Use Cases
1. Data Privacy / Encapsulation
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return balance;
}
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return balance;
}
return "Insufficient funds";
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.withdraw(30); // 120
// balance is protected - no direct access
2. Function Factories
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
3. Memoization (Caching)
function memoize(fn) {
const cache = {}; // Closure over cache
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
console.log("From cache");
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const expensiveOperation = memoize((n) => {
console.log("Computing...");
return n * n;
});
expensiveOperation(5); // "Computing..." → 25
expensiveOperation(5); // "From cache" → 25
Common Pitfall: Closures in Loops
A classic closure gotcha with var:
// ❌ Problem: All callbacks share the same 'i'
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints 3, 3, 3
}, 1000);
}
// ✅ Solution 1: Use let (block scoping)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints 0, 1, 2
}, 1000);
}
// ✅ Solution 2: Create a closure with IIFE
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // Prints 0, 1, 2
}, 1000);
})(i);
}
Event Handlers and Closures
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
button.addEventListener("click", function() {
// This handler closes over 'message'
alert(message);
});
}
setupButton("btn1", "Hello!");
setupButton("btn2", "Goodbye!");
💡 Key Takeaways
- • Closures give functions memory - they remember their lexical environment
- • Use closures for data privacy, function factories, and memoization
- • Use
letinstead ofvarin loops to avoid closure issues - • Closures keep references to variables, not copies of values
- • Be mindful of memory - closures keep outer variables in memory