Closure Explained with a Counter: The JavaScript Interview Question Everyone Fails
Here is a JavaScript interview question that looks simple but exposes whether someone actually understands closures:
function createCounter() {
let count = 0;
return function () {
return ++count;
};
}
const counter = createCounter();
console.log(counter()); // ?
console.log(counter()); // ?
console.log(counter()); // ?
The output is:
1
2
3
Why does count not disappear after createCounter() finishes? The answer is closure.
What Are JavaScript Closures?
A closure is a function that remembers and can access variables from its surrounding lexical scope, even after the outer function has finished executing.
In the example, createCounter() creates the variable count and returns an inner function. That inner function keeps a reference to count.
When this line runs:
const counter = createCounter();
JavaScript does not simply return an isolated function with no context. The returned function retains access to the environment where it was created.
Each time counter() runs, it updates the same captured variable:
- First call:
countchanges from0to1. - Second call:
countchanges from1to2. - Third call:
countchanges from2to3.
JavaScript Closure and Lexical Scope
Closures work because JavaScript uses lexical scoping. A function can access variables based on where it was defined, not where it is called.
function createGreeting() {
const message = "Hello";
return function greet(name) {
return `${message}, ${name}`;
};
}
const greet = createGreeting();
console.log(greet("Rashid")); // Hello, Rashid
The greet() function still accesses message, even though createGreeting() has already returned.
This is the central idea behind a JavaScript closure: the function retains access to the variables it needs from its creation environment.
Each Counter Has Its Own Private State
Closures become more interesting when you create multiple counters:
function createCounter() {
let count = 0;
return function () {
return ++count;
};
}
const firstCounter = createCounter();
const secondCounter = createCounter();
console.log(firstCounter()); // 1
console.log(firstCounter()); // 2
console.log(secondCounter()); // 1
console.log(secondCounter()); // 2
Each call to createCounter() creates a separate lexical environment. Therefore, firstCounter and secondCounter do not share the same count.
This makes closures useful for creating simple forms of private variables without exposing the internal state directly.
Real-World Uses of JavaScript Closures
Closures are used in many ordinary JavaScript features and patterns:
- Event handlers: Remembering configuration or state.
- Timers: Accessing variables when a callback executes later.
- Function factories: Creating customized functions.
- Memoization: Caching previously calculated results.
- Encapsulation: Keeping implementation details private.
function createMultiplier(multiplier) {
return function (value) {
return value * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Common Closure Gotchas
1. Closures and garbage collection
A closure does not automatically create a memory leak. Its captured variables remain available while the closure is still reachable—for example, when it is stored in an active event listener, timer, object, or application-level collection.
When the closure and everything referencing it become unreachable, JavaScript’s garbage collector can reclaim the closure and its captured environment, assuming no other references remain.
The practical risk is accidentally keeping a closure alive longer than necessary, especially when it captures large objects, DOM nodes, or other application data. Remove unused event listeners, clear unnecessary timers, and avoid storing short-lived closures in long-lived objects.
2. var can expose loop-related surprises
Closures created inside loops using var often share the same function-scoped variable. Using let generally creates a separate binding for each loop iteration.
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 0
// 1
// 2
3. A closure is not a snapshot of every variable
A closure retains access to its lexical environment. If a captured variable changes, later calls can observe that changed value. Thinking of a closure as a frozen copy of the surrounding scope often leads to incorrect predictions.
Once you understand lexical scope and retained execution environments, the counter question is no longer a trick. It becomes a straightforward example of how JavaScript functions can carry state across calls—a behavior used everywhere from event-driven interfaces to reusable function factories.
Comments
No comments yet. Be the first to share your thoughts.