How Closures Work in JavaScript: Complete Guide with Practical Examples
A closure is a function that retains access to its surrounding lexical scope's variables even after the outer function has finished executing, enabling powerful patterns like data encapsulation, function factories, and memoization.
This article explores how closures work in JavaScript using practical examples from the leonardomso/33-js-concepts repository, a comprehensive collection of fundamental JavaScript concepts with test-driven implementations.
What Is a JavaScript Closure?
A closure is created when a function is defined inside another function and references variables from the outer function's scope. When the inner function is returned or passed elsewhere, it maintains a hidden reference to the lexical environment—the collection of variables that were in scope at the time of its creation.
Unlike the execution context (which is discarded when a function returns), the lexical environment remains alive as long as the inner function exists. This allows the closure to read and modify captured variables indefinitely.
How Closures Work Under the Hood
Lexical Scoping vs. Dynamic Scoping
JavaScript uses lexical scoping, meaning variable resolution happens based on where the code is written, not where it is called. When a function is created at lines 75-80 in tests/fundamentals/scope-and-closures/scope-and-closures.test.js, JavaScript captures the entire variable environment at that moment.
The Lifetime of Variables
Variables defined in the outer function survive because the inner function maintains a reference to their containing environment. As implemented in leonardomso/33-js-concepts, this mechanism enables private state that persists across multiple invocations without polluting the global scope.
Practical Use Cases Demonstrated in 33-js-concepts
The repository's test suite at tests/fundamentals/scope-and-closures/scope-and-closures.test.js demonstrates several production-ready closure patterns:
Basic Closure with createGreeter
The simplest form of closure captures a single value from the outer scope. In lines 75-80, the createGreeter function demonstrates how a returned function retains access to the greeting parameter:
function createGreeter(greeting) {
// `greeting` is captured in the returned function's lexical environment
return function (name) {
return `${greeting}, ${name}!`;
};
}
const sayHello = createGreeter('Hello');
console.log(sayHello('Alice')); // → "Hello, Alice!"
Data Privacy and Encapsulation
Closures enable true private variables by hiding state within the lexical environment. The createCounter implementation at lines 92-100 keeps the count variable inaccessible from outside while exposing controlled methods:
function createCounter() {
let count = 0; // private variable
return {
inc() { return ++count; },
dec() { return --count; },
value() { return count; }
};
}
const ctr = createCounter();
ctr.inc(); // 1
ctr.inc(); // 2
ctr.dec(); // 1
console.log(ctr.value()); // 1
// `count` is not reachable outside the closure
Function Factories with createMultiplier
At lines 121-130, the repository demonstrates function factories—functions that generate specialized functions on demand. Each invocation creates a new closure with its own captured multiplier value:
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
State Preservation in Event Handlers
The setupClickCounter pattern at lines 154-160 shows how closures maintain state across asynchronous events. The handler increments an internal clicks variable that survives between event invocations without global namespace pollution.
Memoization for Performance Optimization
Lines 172-182 implement createMemoizedFunction, using a closure-held cache object to store expensive computation results:
function memoize(fn) {
const cache = new Map(); // lives in the closure
return function (arg) {
if (cache.has(arg)) return cache.get(arg);
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
const slowSquare = n => { /* heavy work */ return n * n; };
const fastSquare = memoize(slowSquare);
fastSquare(5); // computes
fastSquare(5); // returns cached value
Avoiding the Classic Closure Trap with Var
The Problem: Shared Bindings in Loops
Lines 108-115 of the test file demonstrate a notorious JavaScript pitfall: when using var inside a loop, all created functions share the same variable binding. The final value persists across all closures:
const callbacks = [];
for (var i = 0; i < 3; i++) {
callbacks.push(function() { return i; });
}
console.log(callbacks[0]()); // 3 (not 0)
console.log(callbacks[1]()); // 3 (not 1)
Solutions: let, IIFE, and forEach
Lines 124-138 provide three solutions to create fresh bindings per iteration:
Using let for block scoping:
const callbacks = [];
for (let i = 0; i < 3; i++) {
callbacks.push(() => i); // each arrow function closes over its own `i`
}
console.log(callbacks[0]()); // 0
console.log(callbacks[1]()); // 1
console.log(callbacks[2]()); // 2
Using an Immediately Invoked Function Expression (IIFE):
for (var i = 0; i < 3; i++) {
(function(capturedI) {
callbacks.push(function() { return capturedI; });
})(i);
}
Summary
- Closures capture lexical scope: A function retains access to its birthplace's variables through a hidden reference to the lexical environment, not the execution context.
- Source implementation: The
leonardomso/33-js-conceptsrepository attests/fundamentals/scope-and-closures/scope-and-closures.test.jsdemonstrates closures from basic variable retention (lines 75-80) to advanced memoization patterns (lines 172-182). - Privacy without classes: Closures provide true encapsulation, hiding state from external access while exposing controlled interfaces via returned objects.
- Factory functions: Use closures to generate specialized functions like
createMultiplierorcreateTaxCalculatorthat remember their configuration. - Avoid var in loops: Use
let, IIFEs, orforEachto prevent all iterations from sharing the same closure binding.
Frequently Asked Questions
What is the difference between lexical scope and closure?
Lexical scope is the static structure of variable accessibility determined by where code is written in the source file. Closure is the runtime mechanism that preserves that scope's variables when a function executes outside its original context. While lexical scope defines what variables are available, closure ensures those variables remain available after the outer function returns.
How do closures enable data privacy in JavaScript?
Closures create private variables by placing them in the outer function's scope and returning only specific methods that can access them. As shown in createCounter (lines 92-100), the count variable exists in the lexical environment but cannot be accessed directly from outside, effectively simulating private class fields in pre-ES2015 JavaScript or in module patterns.
Why do loops with var create closure bugs and how do you fix them?
var is function-scoped, meaning all iterations of a loop share the same variable reference. When closures are created inside the loop, they all capture that single shared reference, resulting in each closure seeing the final value. Fix this by using let (block-scoped, creating a new binding per iteration), wrapping the loop body in an IIFE to create a new scope, or using forEach instead of a for loop.
When should you avoid using closures?
Avoid closures when memory leaks are a concern—captured variables remain in memory as long as the closure exists, which can prevent garbage collection of large datasets. They are also unnecessary for simple, one-off operations where passing explicit parameters is clearer. Additionally, avoid overusing closures in tight loops where the overhead of scope chain lookup might impact performance, though modern JavaScript engines optimize this well.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →