JavaScript Closures
A closure is a function that remembers and keeps access to variables from the scope where it was created, even after that outer function has finished running.
What Is a Closure?
In JavaScript, functions can be defined inside other functions. An inner function has access to the variables of the function that surrounds it. A closure is formed when that inner function is returned or otherwise kept alive, so it continues to reference those outer variables long after the outer function has returned. The inner function carries its birthplace with it, keeping those variables available.
A function that remembers its surroundings
<!DOCTYPE html>
<html>
<body>
<h2>A function that remembers its surroundings</h2>
<script>
function makeGreeter(greeting) {
// 'greeting' lives in the outer scope
return function (name) {
// The inner function still sees 'greeting'
return greeting + ", " + name + "!";
};
}
const sayHi = makeGreeter("Hi");
const sayHello = makeGreeter("Hello");
console.log(sayHi("Ada")); // "Hi, Ada!"
console.log(sayHello("Grace")); // "Hello, Grace!"
</script>
</body>
</html>Each call to makeGreeter creates a new scope with its own greeting value. The returned function keeps a private link to that specific value, which is why sayHi and sayHello behave differently even though they came from the same factory.
Private State with Closures
Closures let you create data that is hidden from the outside world. Variables declared inside the outer function cannot be read or changed directly; they can only be reached through the inner functions you expose. This is a common pattern for building counters, caches, and modules with private state.
A counter with a private variable
<!DOCTYPE html>
<html>
<body>
<h2>A counter with a private variable</h2>
<script>
function createCounter() {
let count = 0; // private, not accessible from outside
return {
increment() { count++; return count; },
decrement() { count--; return count; },
value() { return count; }
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.value()); // 1
// 'count' cannot be touched directly from here
</script>
</body>
</html>A Classic Loop Gotcha
Closures capture variables, not copies of their values at a moment in time. A well-known trap appears when creating functions in a loop with var, because all functions end up sharing the same variable. Using let, which is block scoped, gives each iteration its own binding and fixes the problem.
Why let fixes closures in loops
<!DOCTYPE html>
<html>
<body>
<h2>Why let fixes closures in loops</h2>
<script>
// With var, every callback shares one 'i'
var fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => i);
}
console.log(fns[0](), fns[1](), fns[2]()); // 3 3 3
// With let, each iteration captures its own 'i'
const fns2 = [];
for (let j = 0; j < 3; j++) {
fns2.push(() => j);
}
console.log(fns2[0](), fns2[1](), fns2[2]()); // 0 1 2
</script>
</body>
</html>Where Closures Show Up
- Data privacy: hiding variables behind returned functions.
- Function factories: generating specialized functions from shared logic.
- Callbacks and event handlers that need to remember context.
- Memoization and caching, where results are stored between calls.
- Maintaining state in asynchronous code such as timers and promises.