Closures
MedA closure is a function that remembers the variables from its outer scope even after the outer function has returned. Think of it as a function carrying a "backpack" of variables wherever it goes.
Interactive Visualization
1function outer() {2 let x = 10;3 function inner() {4 return x;5 }6 return inner;7}89const fn = outer();10fn(); // 10
Understanding Closures
A closure is one of the most important concepts in JavaScript, and understanding it deeply is essential for writing clean, modular code. At its core, a closure is created when a function retains access to variables from its surrounding lexical scope, even after that outer function has finished executing. Every time you define a function inside another function, you create a closure.
The reason closures work is tied to how JavaScript manages scope. When a function is created, it captures a reference to the variables in its enclosing environment — not a snapshot of their values. This means the inner function can read and modify those variables long after the outer function has returned. Think of it as the function carrying an invisible backpack of all the variables it might need.
Closures enable several powerful patterns in JavaScript. The most common is data privacy — by wrapping variables inside a function and only exposing controlled access through returned methods, you can create private state that cannot be tampered with from outside. This is the foundation of the module pattern, which was the standard way to organize JavaScript code before ES modules existed.
Another practical use is creating function factories. A factory function takes configuration parameters and returns a new function that has those parameters baked in via closure. This is how techniques like partial application and currying work. Memoization, where a function caches its results for repeated inputs, also relies on closures to maintain the cache between calls.
The classic closure interview question involves closures inside loops. When using var in a for loop with setTimeout, all callbacks share the same variable and print the final value. The fix is to use let (which creates a new binding per iteration) or to wrap the callback in an IIFE that captures the current value. Understanding why this happens — captured by reference, not by value — is the key insight interviewers are testing for.
One thing to be mindful of is memory. Since closures keep references to outer variables alive, those variables cannot be garbage collected as long as the closure exists. In most cases this is harmless, but if a closure accidentally captures a large object or DOM element, it can cause memory leaks. Removing event listeners and nullifying references when they are no longer needed helps prevent this.
Key Points
- A closure is created every time a function is created
- Inner functions have access to outer function variables
- The outer variables are captured by reference, not by value
- Closures enable data privacy and stateful functions
Code Examples
Basic Closure
function outer() { let count = 0; return function inner() { count++; return count; }; } const counter = outer(); counter(); // 1 counter(); // 2 counter(); // 3
Inner function remembers outer variables
Counter Example
function createCounter(start) { let count = start; return { increment: () => ++count, decrement: () => --count, getValue: () => count }; } const counter = createCounter(10); counter.increment(); // 11 counter.increment(); // 12 counter.getValue(); // 12
Each call uses the same closed-over variable
Private Variables
function createAccount(initial) { let balance = initial; // private! return { deposit(amount) { balance += amount; return balance; }, withdraw(amount) { if (amount > balance) { return "Insufficient funds"; } balance -= amount; return balance; }, getBalance() { return balance; } }; } const account = createAccount(100); account.deposit(50); // 150 account.balance; // undefined (private!)
Closures enable data privacy
Multiple Closures
function createCounter(start) { let count = start; return () => count++; } const counterA = createCounter(0); const counterB = createCounter(100); counterA(); // 0 counterA(); // 1 counterB(); // 100 counterB(); // 101 // Each closure has independent state
Each closure has its own independent state
Loop Closure Bug
// THE BUG: All callbacks share same i for (var i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 100); } // Output: 3, 3, 3 // Why? var is function-scoped // All 3 closures reference the // SAME i, which is 3 after loop
Classic var loop closure problem
Loop Fix with let
// THE FIX: let creates new binding for (let i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 100); } // Output: 0, 1, 2 // Why? let is block-scoped // Each iteration gets its own i // Each closure captures different i
let creates new binding per iteration
Common Mistakes
- Loop variable capture - all callbacks share the same variable
- Memory leaks from closures holding large objects
- Forgetting that closures capture by reference, not value
Interview Tips
- Use the "backpack" metaphor to explain closures
- Show practical uses: data privacy, function factories, memoization
- Be ready to solve the classic loop closure problem