Closures & Scope Chain
How functions 'remember' variables from their creation context β the foundation of data privacy, callbacks, and functional patterns in JavaScript.
A closure is created every time a function is defined inside another function and references the outer function's variables. The inner function 'closes over' those variables, keeping them alive even after the outer function has returned.
JavaScript uses lexical (static) scoping β a function's accessible variables are determined by WHERE it's written in the source code, not where it's called. Inner functions can access variables from all enclosing scopes.
When a function is created, the engine attaches a hidden [[Scope]] property linking to the parent scope's variable environment. This forms a chain: local scope β parent scope β β¦ β global scope.
A closure occurs when a function references variables from an outer scope that has finished executing. Normally, local variables are garbage-collected when a function returns. But if an inner function still references them, the engine keeps those variables alive in a 'closure' object.
When a variable is accessed, the engine walks the scope chain: 1) Check local scope β 2) Check enclosing scope β 3) Keep walking up β 4) Global scope β 5) ReferenceError. This lookup happens EVERY time the variable is accessed.
Key Concepts
A function bundled with references to its surrounding state (lexical environment). The closed-over variables persist even after the outer function returns.
An internal structure that holds variable bindings for a specific scope. Each function execution creates a new lexical environment.
The linked list of lexical environments that the engine traverses when resolving variable references, from innermost to outermost scope.
'var' is function-scoped (shared across loop iterations). 'let' and 'const' are block-scoped (each iteration gets its own binding).
1// A closure is a function that "remembers"2// variables from its creation scope:34function createMultiplier(factor) {5 // 'factor' is enclosed in the closure6 return function (number) {7 return number * factor;8 };9}1011const double = createMultiplier(2);12const triple = createMultiplier(3);1314double(5); // 10 β uses factor=215triple(5); // 15 β uses factor=31617// Each call to createMultiplier() creates18// a NEW closure with its OWN 'factor'
Closures power most JavaScript patterns: React hooks (useState, useEffect), Node.js middleware, event handlers, callbacks, currying, memoization, and the module pattern. Understanding closures is essential for debugging variable reference issues.
Common Pitfalls
1Data Privacy with Module Pattern
You need a counter module where 'count' cannot be directly modified from outside β only through increment/decrement methods. No class syntax, just functions.
Without closures, you'd store count as a global variable or property, making it accessible and modifiable by any code. This breaks encapsulation and makes bugs harder to track.
Use an IIFE (Immediately Invoked Function Expression) that returns an object with methods. The 'count' variable lives in the IIFE's closure, invisible to the outside world but accessible to the returned methods.
Takeaway: Closures are JavaScript's primary mechanism for data privacy. Before ES2022's #private fields, closures were the ONLY way to create truly private variables in JavaScript.
2The Classic Loop + setTimeout Bug
You create 5 buttons in a loop, each supposed to alert its index when clicked. But every button alerts '5' β the final loop value.
Using 'var' in a for-loop creates a single shared binding. All 5 click handlers close over the SAME 'i' variable. By the time any handler runs, the loop has finished and i === 5.
Replace 'var' with 'let' (creates a new binding per iteration), or wrap the handler in an IIFE that captures the current value: (function(j) { button.onclick = () => alert(j); })(i). Both create a separate closure per iteration.
Takeaway: This is the most common closure bug in JavaScript. Understanding that 'var' is function-scoped (one binding) while 'let' is block-scoped (one binding per iteration) eliminates this entire class of bugs.