Closures in JavaScript
A closure is one of the most powerful and heavily tested concepts in JavaScript. Simply put, a closure is a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
In other words, a closure gives you access to an outer function's scope from an inner function, even after the outer function has finished executing.
1. Lexical Scoping
To understand closures, you must first understand lexical scoping. In JavaScript, the scope of a variable is determined by its location within the source code. Inner functions have access to variables declared in their outer scope.
function outer() {
const message = "Hello from outer scope!";
function inner() {
// 'inner' can access 'message' due to lexical scoping
console.log(message);
}
inner();
}
outer(); // "Hello from outer scope!"
2. Creating a Closure
A closure is created every time a function is created, at function creation time. The magic happens when the inner function is returned from the outer function before being executed.
function makeCounter() {
let count = 0; // 'count' is local to makeCounter
return function() {
count += 1; // The inner function accesses and modifies 'count'
return count;
};
}
// makeCounter runs and returns the inner function.
// The execution context of makeCounter is destroyed, but the inner function
// retains a reference to 'count' because of the closure.
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Even though makeCounter has finished running, the returned function maintains a live reference to the count variable.
3. Practical Use Cases
Closures aren't just interview trivia; they are used constantly in real-world code.
A. Data Privacy (Emulating Private Methods)
Before ES2022 introduced private class fields (#), closures were the primary way to create private variables in JavaScript.
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable
return {
deposit: function(amount) {
balance += amount;
return balance;
},
withdraw: function(amount) {
if (amount <= balance) {
balance -= amount;
return balance;
}
return "Insufficient funds";
},
getBalance: function() {
return balance;
}
};
}
const account = createBankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
console.log(account.balance); // undefined (cannot access directly)
B. Currying and Partial Application
Closures allow functions to remember arguments passed to them in stages. (See the Currying lesson for more details).
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
console.log(double(5)); // 10
C. Callbacks and Event Listeners
When you attach an event listener, the callback function often forms a closure over variables in the scope where the listener was defined.
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
// The callback remembers 'message'
button.addEventListener('click', () => {
alert(message);
});
}
4. The Module Pattern
The Module Pattern uses closures to create a self-contained module with private state and a public API. It often utilizes an IIFE (Immediately Invoked Function Expression).
const CalculatorModule = (function() {
// Private state
let history = [];
function log(operation) {
history.push(operation);
}
// Public API
return {
add: function(a, b) {
const result = a + b;
log(`${a} + ${b} = ${result}`);
return result;
},
getHistory: function() {
return [...history]; // Return a copy to prevent mutation
}
};
})();
CalculatorModule.add(5, 3);
console.log(CalculatorModule.getHistory()); // ["5 + 3 = 8"]
// CalculatorModule.history is undefined
Summary
- A closure is a function combined with its lexical environment.
- It allows a function to remember and access variables from its outer scope, even after that outer function has returned.
- Closures are heavily used for data privacy, currying, event listeners, and the module pattern.