Closures in JavaScript: The Key to Lexical Scope

A function is created within the lexical environment in which it was declared, so it can continue to access those surrounding bindings when it runs elsewhere. A closure is the combination of that function and the lexical environment it retains access to.
What are Closures?
Basically, a closure is a function that 'remembers' its outer scope even when executed outside of that scope. When a function is declared, it captures the variables from its surrounding environment. This captured environment is preserved even if the outer function has already finished execution.
Here's a simple example:
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3In this example, the inner function has access to the count variable, even though the createCounter function has already returned. This is a closure in action.
How Do Closures Work?
To understand closures, it's important that we first grasp JavaScript's lexical scoping. When a function is created, it carries along a reference to its outer scope, forming a "chain" of scopes called the scope chain. This mechanism allows the inner function to access variables declared in its parent scope.
The Scope Chain in Action
Here's an example that ‑ hopefully ‑ illustrates what I'm trying to describe:
function outer() {
let outerVar = 'I am from the outer scope';
function inner() {
console.log(outerVar);
}
return inner;
}
const innerFunc = outer();
innerFunc(); // Logs: "I am from the outer scope"Here, the inner function retains access to outerVar because of the scope chain established when the inner function was created.
Practical Applications of Closures
Closures aren't just an interesting quirk of JavaScript, they are incredibly useful in everyday programming. Let's look at a few practical scenarios I've cobbled together.
1. Encapsulation and Data Privacy
Closures allow us to emulate private variables, which are not directly accessible from outside the function:
function createSecret(secret) {
return function () {
return secret;
};
}
const getSecret = createSecret('hidden value');
console.log(getSecret()); // 'hidden value'Here, the secret variable remains private, accessible only through the returned function.
2. Maintaining State
Closures are ideal for maintaining state between function calls, making them invaluable for scenarios where you need a private or persistent value that isn't directly accessible from the global scope. Consider the counter example I've used above: each time the returned function is called, it can access and update the count variable, which remains private to the enclosing function.
This approach is useful in reusable components, utilities and libraries. A debounce function, for example, can keep track of its timer without putting that state on a global variable. Closures limit which parts of your code can directly access a binding, but they do not make a client‑side token secret or create a security boundary.
3. Function Factories
Closures allow us to create functions with shared behaviours, for example:
function multiplyBy(multiplier) {
return function (num) {
return num * multiplier;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5)); // 10
console.log(triple(5)); // 154. Event Listeners
Closures are often used in event listeners to capture and retain values from their outer scope:
function setupListeners() {
for (let i = 1; i <= 3; i++) {
document.querySelector(`#button${i}`).addEventListener('click', function () {
console.log(`Button ${i} clicked`);
});
}
}
setupListeners();Common Pitfalls and Considerations
Whilst closures are powerful, they can sometimes lead to unintended behaviours, especially when working with loops.
Closures and Loops
A classic pitfall involves closures in loops, where all closures created within the loop share the same variable:
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// Logs: 4, 4, 4This happens because var creates a single variable shared across iterations. Using let instead solves this:
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// Logs: 1, 2, 3Wrapping Up
Key Takeaways
- Closures allow functions to access variables from their parent scope, even after the parent function has executed.
- They are a direct result of JavaScript's lexical scoping and the scope chain.
- Practical uses include data privacy, maintaining state, and creating function factories.
- Be cautious of common pitfalls, such as closures in loops.
Closures are useful because functions can carry the state they need with them. The same behaviour can also retain more memory than expected or capture the wrong loop variable. Use the pattern deliberately, keep the captured scope small, and check the lifetime of callbacks that remain registered for a long time.
Postscript
February 2019: React 16.8 introduced Hooks, including useState and useReducer. React keeps their state between renders. A callback created during a render closes over that render's bindings, so a callback retained from an earlier render can still read an older state value. That is the stale‑closure problem: the closure retains its surrounding bindings; it is not what stores React's next state.