JavaScript Closures
Understand how closures allow inner functions to retain access to variables from parent scopes with examples.
TL;DR
- 01Ensure inner functions retain access to their defining parent scopes.
- 02Store persistent private data state safely without using global variables.
- 03Resolve outer variables based on where functions are statically defined.
Tips
- 01Expose public API methods while keeping raw state hidden inside an enclosing closure scope function.
- 02Choose closures over standard class definitions when you only need to store small private states.
Warnings
- 01Avoid creating unnecessary closures enclosing huge objects because they can generate substantial memory leaks.
- 02Declare loop indexes using
letso that each iteration receives its own distinct variable binding.
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;
};
}In Practice
A debounce factory function closes over a timer reference to ensure that rapid handlers only execute once typing pauses.
- 01Declare a local variable to hold the active timeout identifier.
- 02Return a closure function that accepts arguments and intercepts calls.
- 03Clear any existing scheduled timeout to cancel the previous call.
- 04Schedule a new timeout to execute the target function after a delay.
- 05Forward 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.
Avoid capturing large object references that you do not need. Destructure only specific values required by the inner function. Nullify large variable handles once they are no longer needed.