A closure is a function bundled together with references to the variables it needs from the surrounding scope. Closures are how JavaScript gives you data privacy, factory functions, and the module pattern. This lesson walks through the scope chain, the loop-closure trap, and the patterns you will use in real code.
Lexical scope and the scope chain
JavaScript uses lexical scope, which means a function can see the variables of the scope where it was defined, not where it is called. The scope chain is the list of enclosing scopes the runtime walks to resolve a name.
const x = "global";
function outer() {
const x = "enclosing";
function inner() {
console.log(x); // "enclosing" -- lexical, not global
}
inner();
}
outer();
What a closure is
When a function references a variable from an enclosing scope and that function is later called from somewhere else, the variable still exists. The function plus that reference is a closure. Closures outlive the function call that created them.
function makeCounter() {
let n = 0;
return function () {
n += 1;
return n;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Factory functions with private state
Closures are the standard way to create objects with private state before the class private fields landed. The state is captured in the closure and unreachable from outside.
function createUser(name, role) {
let password = "secret"; // private
return {
name,
role,
checkPassword(input) {
return input === password;
},
setPassword(newPassword) {
password = newPassword;
},
};
}
const u = createUser("Asha", "admin");
console.log(u.name); // Asha
console.log(u.checkPassword("secret")); // true
u.setPassword("newpass");
console.log(u.checkPassword("newpass")); // true
The classic loop closure trap
var has function scope, so all iterations of a for loop share the same variable. Closures created inside the loop all see the final value. let introduces a new binding per iteration, which fixes the bug. Use let (or const) inside loops.
// BROKEN with var
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => i);
}
console.log(fns.map(f => f())); // [3, 3, 3] -- all see the final i
// FIXED with let
const fns2 = [];
for (let i = 0; i < 3; i++) {
fns2.push(() => i);
}
console.log(fns2.map(f => f())); // [0, 1, 2] -- each sees its own i
The module pattern
Use an IIFE that returns an object to expose some functions while keeping others private. The private functions are captured in the closure. Modern ES modules make this cleaner, but you will see the module pattern in older code.
const counter = (function () {
let n = 0;
return {
inc() { n += 1; },
value() { return n; },
};
})();
counter.inc();
counter.inc();
console.log(counter.value()); // 2
// counter.n is not accessible
Summary
Closures are how functions remember the variables of the scope where they were defined. Use them for factory functions, private state, and the module pattern. Always use let (not var) inside loops when you capture the index in a closure. The next lesson covers strings, template literals, and the modern string methods.
Closures and memory
Closures keep their captured variables alive as long as the closure is reachable. A common memory leak is creating closures that capture large objects and then keeping those closures alive longer than needed. For example, attaching many event listeners to a long-lived parent DOM element can pin large structures in memory even after the listeners should be gone. Modern engines optimize the common cases, but it is good practice to remove listeners (removeEventListener) and clear references (set the variable holding the closure to null) when you are done with them.
function attach() {
const big = new Array(1_000_000).fill("data");
document.addEventListener("click", () => {
console.log(big.length); // pins `big` in memory
});
}
// Cleanup pattern
function attachAndCleanup() {
const big = new Array(1_000_000).fill("data");
const handler = () => console.log(big.length);
document.addEventListener("click", handler);
return () => document.removeEventListener("click", handler); // teardown
}
const cleanup = attachAndCleanup();
// ...later
cleanup(); // releases the closure and `big`
Best practices
Reach for closures when you need private state or factory functions. Avoid nesting closures more than two or three levels deep; the cognitive overhead grows fast. Prefer classes with private fields for new code; the syntax is clearer than the IIFE pattern. Always use let (not var) when capturing loop indices in closures. Finally, be cautious about closures in long-lived callbacks and event listeners; release them when you are done to keep memory flat.
Comparison with Python
Python also has closures, but the rules are slightly different. Python's closures cannot see enclosing variables for assignment without a nonlocal declaration; they can read them just fine. JavaScript's closures can read and write enclosing variables freely. Python's factory functions return objects or closures the same way JavaScript's do, and Python's class with private prefix (_password) is roughly equivalent to JavaScript's #privateField. Python's nonlocal is what JavaScript's var used to be before let; both let you rebind an enclosing variable from a nested function.