JavaScript Closures Explained
If you've spent any amount of time writing JavaScript, you've almost certainly used closures—whether you realized it or not. Closures are often cited as one of the most confusing concepts for developers to grasp, but they form the backbone of many advanced patterns in the language.
In this guide, we'll break down what closures are, explore how lexical scoping makes them possible, and look at some practical examples you can use in your daily workflow.
What is a Closure?
At its most fundamental level, MDN defines a closure like this:
"A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment)."
In simpler terms: A closure gives a function access to its outer scope, even after that outer function has returned.
To understand this, we first need to understand how JavaScript handles variable scope.
The Foundation: Lexical Scoping
JavaScript uses lexical scoping. This means that the scope of a variable is determined by its physical location within the written code. Nested functions have access to variables declared in their outer scope.
function init() {
const name = 'Mozilla'; // `name` is a local variable created by init
function displayName() {
// displayName() is the inner function, a closure
console.log(name); // uses variable declared in the parent function
}
displayName();
}
init(); // Output: "Mozilla"In the example above, displayName has access to the name variable of the init function. This isn't a closure yet—it's just lexical scoping at work.
Creating a Real Closure
A closure truly happens when an inner function is exported (or returned) outside of its parent function, but still retains access to the variables declared in the parent's scope.
Let's modify the previous example:
function makeFunc() {
const name = 'Hridoy';
function displayName() {
console.log(name);
}
return displayName; // We are returning the inner function itself, not executing it!
}
const myFunc = makeFunc(); // makeFunc finishes executing
myFunc(); // Output: "Hridoy"Normally, local variables within a function only exist for the duration of that function's execution. Once makeFunc() finishes running, you would expect the name variable to no longer be accessible.
However, JavaScript engines realize that displayName still needs access to name, so they keep that environment alive. The myFunc variable isn't just a reference to a function; it's a reference to a closure—the function code combined with the lexical environment where it was created.
Practical Use Cases for Closures
Understanding closures is great, but how do we actually use them to write cleaner, more effective JavaScript?
1. Data Privacy and Encapsulation
Before classes and native private fields (#) were introduced in ES6, closures were the primary way to emulate private methods and properties. This is known as the Module Pattern.
function counter() {
// private variable
let count = 0;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const myCounter = counter();
console.log(myCounter.increment()); // 1
console.log(myCounter.increment()); // 2
console.log(myCounter.getCount()); // 2
// There is absolutely no way to directly access or modify `count`
console.log(myCounter.count); // undefinedBecause count is only accessible through the returned object's methods, it is perfectly encapsulated.
2. Function Factories
Closures allow you to create "factory" functions that generate other specialized functions. This is a core concept in functional programming patterns like currying.
function makeAdder(x) {
// `x` is trapped in the closure
return function(y) {
return x + y;
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12Both add5 and add10 share the same function body definition, but they store different lexical environments. In add5's environment, x is 5. In add10's environment, x is 10.
3. Event Listeners and Callbacks
If you've written event listeners, you've used closures. Callbacks frequently need to access variables from the scope where they were defined long after that code has executed.
function attachHandler(elementId, message) {
const el = document.getElementById(elementId);
// The click callback forms a closure that traps the `message` variable
el.addEventListener('click', function() {
console.log(`Button clicked! Message: ${message}`);
});
}
attachHandler('submit-btn', 'Form submitted successfully.');The Classic Closure Pitfall: Loops
One of the most infamous JavaScript bugs involves closures inside loops using the older var keyword.
// THE PROBLEM
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Outputs: 3, 3, 3Why does this happen? The setTimeout callbacks form closures over the var i variable, which is function-scoped. By the time the 1000ms delay finishes and the callbacks execute, the loop has already completed and i has been incremented to 3. All three timeouts reference the exact same variable i.
The Solution: let
The modern fix is to simply use let, which creates block-level scope. A new i variable is created for every single iteration of the loop.
// THE SOLUTION
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Outputs: 0, 1, 2Wrapping Up
Closures aren't magic—they are simply JavaScript's way of ensuring functions retain access to the environment in which they were born. Mastering closures will instantly level up your ability to:
- Write robust module patterns and encapsulate data.
- Build flexible, reusable higher-order logic.
- Handle asynchronous callbacks without state leaking.
The next time you write an anonymous function or an event listener, take a second to look at the variables it uses. You might just be looking at a closure.
