JavaScript Closures
Understand how closures allow inner functions to retain access to variables from parent scopes with examples.
TL;DR
- Ensure inner functions retain access to their defining parent scopes.
- Store persistent private data state safely without using global variables.
- Resolve outer variables based on where functions are statically defined.
What Closures Are
Closure definitionKeeps reference access to outer scope variables even after parent execution finishes.
function outer() {
let n = 0;
return () => ++n;
}
const count = outer();
count(); // 1Scope nestingForms closures automatically whenever you nest child functions inside parent contexts.
function parent() {
const x = 1;
function child() { return x; }
}Memory persistenceRetains outer scope values in memory as long as the child function exists.
const fn = outer(); // n stays in memoryLexical scopeResolves variable scopes statically based on where the functions are declared.
const x = 10;
function test() { console.log(x); }Closures and Loops
var in loopShares a single variable reference across all loop callbacks, causing bugs.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
// logs 3, 3, 3let in loopCreates a new variable binding block per loop iteration to fix sharing.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
// logs 0, 1, 2IIFE captureCaps variable values per iteration loop by wrapping functions in IIFE scopes.
for (var i = 0; i < 3; i++) {
(v => setTimeout(() => console.log(v)))(i);
}Private Variables
State encapsulationStores internal variable values safely away from the global execution context.
function createCounter() {
let count = 0;
return {
increment: () => ++count,
get: () => count
};
}Public API accessExposes interface methods to read and write private variables under control.
const c = createCounter();
c.increment();
console.log(c.get()); // 1Accidental mutationPrevents external script scripts from corrupting or writing internal state values directly.
let c = createCounter();
// c.count is undefinedFunction Factories
Behavior configurationCreates functions sharing standard behaviors but retaining distinct internal configurations.
function makeAdder(x) {
return y => x + y;
}
const add5 = makeAdder(5);
add5(10); // 15Private memoizationCloses over a private Map cache to return cached function outputs.
function memoize(fn) {
const cache = new Map();
return x => {
if (cache.has(x)) return cache.get(x);
const res = fn(x);
cache.set(x, res);
return res;
};
}Tips
- Expose public API methods while keeping raw state hidden inside an enclosing closure scope function.
- Choose closures over standard class definitions when you only need to store small private states.
Warnings
- Avoid creating unnecessary closures enclosing huge objects because they can generate substantial memory leaks.
- Declare loop indexes using
letso that each iteration receives its own distinct variable binding.
In Practice
A debounce factory function closes over a timer reference to ensure that rapid handlers only execute once typing pauses.
- Declare a local variable to hold the active timeout identifier.
- Return a closure function that accepts arguments and intercepts calls.
- Clear any existing scheduled timeout to cancel the previous call.
- Schedule a new timeout to execute the target function after a delay.
- Forward the function arguments to the target handler on execution.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}FAQ
Closures inside a var loop share one variable reference. The loop terminates before the callbacks execute. Change the declaration to let to bind a fresh variable index per iteration.
Declare local variables inside a parent function and return helper functions accessing them. The returned helpers close over the state. External operations cannot inspect or alter this private state directly.
A function becomes a closure when it references variables outside its scope after the parent context exits. The closure keeps these external variables alive in memory. Normal functions only use parameters.
A factory function accepts configuration values and returns specialized functions enclosing those values. For example, makeAdder(5) returns a helper that always adds five. This keeps logic parameterized and clean.