Closures are one of the most powerful and advanced concepts in JavaScript. They are at the heart of data privacy, encapsulation, callbacks, and functional programming.
A closure is a function that remembers the variables from its outer scope, even after that outer function has finished executing. In other words, a closure gives inner functions access to the outer function’s variables, even after the outer function is gone from the call stack.
The Core Idea
function outer() {
let name = "John";
function inner() {
console.log("Hello " + name);
}
return inner;
}
const greet = outer(); // outer() executed, inner() returned
greet(); // "Hello John"
What’s happening?
- outer() executes and creates a function execution context with local variable name
- It returns the inner function, not calling it yet
- When we call greet() (which is actually inner()), the outer function’s context is gone from the call stack
- But the inner function still remembers the variable name via its closure. It holds a reference to outer’s lexical environment
That memory of variables is what we call a closure.
Formal Definition
A closure is the combination of a function and its lexical environment within which it was declared.
When a function is defined, JavaScript captures its surrounding scope (the lexical environment), not the call-time environment.
How Closures Work Internally
A Lexical Environment is made up of:
{
Environment Record: { local variables },
Outer Environment Reference: pointer to parent Lexical Environment
}
When the JavaScript engine defines a function, it stores the outer environment reference of where that function was created. When executed, that reference allows the function to walk up the scope chain to find variables.
Step-by-Step Example
function outer() {
let counter = 0;
function inner() {
counter++;
console.log(counter);
}
return inner;
}
const fn = outer();
fn(); // 1
fn(); // 2
Flow
- outer() runs and creates local variable counter = 0
- inner() is defined and its closure closes over counter
- outer() returns inner
- Normally, counter would be destroyed when outer() finishes, but since inner() still references it, the memory stays alive
- Each call to fn() increments the same counter
The function fn has a persistent private state.
Visualization
Global Execution Context
│
├── outer() Execution Context
│ ├── counter = 0
│ └── inner() function → closure created
│
└── inner() Execution Context (later)
└── Accesses counter from outer()'s lexical environment
Closures Preserve References, Not Copies
A closure doesn’t copy values. It preserves references.
function makeCounter() {
let count = 0;
return () => ++count;
}
const c1 = makeCounter();
const c2 = makeCounter();
console.log(c1()); // 1
console.log(c1()); // 2
console.log(c2()); // 1
Each call to makeCounter() creates a new lexical environment. So c1 and c2 have independent closures.
Closure in Loops Example
Common interview question:
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000);
}
Output:
4
4
4
Why? var is function-scoped, not block-scoped. All closures refer to the same i, which becomes 4 after the loop.
Fix with let:
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000);
}
Fix with IIFE:
for (var i = 1; i <= 3; i++) {
(function(i) {
setTimeout(() => console.log(i), 1000);
})(i);
}
Both create a new lexical environment for each iteration.
Data Privacy with Closures
Closures allow private variables, a key feature in functional design.
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
console.log("Deposited:", amount);
},
getBalance() {
console.log("Balance:", balance);
}
};
}
const acc = createBankAccount(100);
acc.deposit(50);
acc.getBalance(); // 150
console.log(acc.balance); // undefined
balance is private and only accessible through the closure functions.
Real-World Applications
| Use Case | Description |
|---|---|
| Data privacy/encapsulation | Protect variables from outside access |
| Callbacks & event handlers | Maintain state across async operations |
| Functional programming | Currying, partial application |
| Memoization | Store previously computed results |
| Module pattern | Create isolated module-level state |
Advanced Example: Function Factories
function makeMultiplier(multiplier) {
return function(num) {
return num * multiplier;
};
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Each returned function has its own closure with a different multiplier.
Closures and Asynchronous Code
Closures are especially important in async operations like setTimeout:
function delayedPrint(msg, delay) {
setTimeout(() => console.log(msg), delay);
}
delayedPrint("Hello!", 2000);
Even after delayedPrint() finishes executing, the inner arrow function still remembers msg thanks to the closure.
Summary
| Concept | Description |
|---|---|
| Closure | Function plus surrounding lexical scope |
| Purpose | Allow inner functions to access outer variables |
| Lifetime | Persists even after outer function returns |
| Used For | Data privacy, async callbacks, functional patterns |
Closure Flow
Function declared → Closure formed (captures outer variables)
↓
Function returned → Outer scope removed from stack
↓
Closure retains reference to those outer variables
↓
Inner function still can read/write them later
Memory Behavior
JavaScript engines use garbage collection, but closures prevent garbage collection from deleting variables that are still referenced. Be careful with large closures inside event handlers. They can cause memory leaks if not managed properly.
A closure is formed when an inner function remembers variables from its lexical scope, even after that scope has finished executing. Closures are essential for data privacy, maintaining state in async operations, and building flexible functional patterns.