At this point, you already know what closures are and how they work.
Now it's time to understand how the JavaScript engine actually implements closures internally.
This is the level of understanding expected from Senior JavaScript Engineers.
How JavaScript Stores Variables
Whenever JavaScript executes your program, it creates something called an Execution Context.
Each execution context contains several internal components.
Conceptually, it looks like this:
Execution Context
├── Variable Environment
│ ├── count
│ ├── user
│ └── total
│
├── Lexical Environment
│
├── Scope Chain
│
└── this
The important parts for closures are:
- Variable Environment
- Lexical Environment
What is the Lexical Environment?
A Lexical Environment is an internal data structure used by the JavaScript engine.
Think of it as a dictionary that stores variables.
Example:
function outer() {
let name = "John";
let age = 30;
}
Internally, JavaScript stores something conceptually like this:
Lexical Environment
name → "John"
age → 30
Every function gets its own Lexical Environment.
Environment Records
The Lexical Environment itself contains another structure called the Environment Record.
Conceptually:
Lexical Environment
↓
Environment Record
↓
Variables
Example:
function user() {
let firstName = "John";
let lastName = "Doe";
}
Environment Record
firstName → John
lastName → Doe
Every variable declared with:
- let
- const
- var
- function
- class
is stored here.
The Outer Environment Reference
Closures exist because every Lexical Environment keeps a reference to its parent.
Imagine this code.
let country = "Egypt";
function outer() {
let city = "Cairo";
function inner() {
console.log(country);
console.log(city);
}
}
Internally
Global Environment
↓
country
↓
Outer Environment
↓
city
↓
Inner Environment
The inner function knows where its parent environment lives.
This relationship forms the Scope Chain.
The Hidden [[Environment]] Property
Every JavaScript function has an internal hidden property.
It isn't accessible from your code.
Conceptually:
Function
↓
[[Environment]]
↓
Lexical Environment
Example
function outer() {
let score = 50;
return function () {
console.log(score);
};
}
When the inner function is created...
JavaScript stores something like:
Inner Function
↓
[[Environment]]
↓
Outer Lexical Environment
That hidden reference is what makes closures possible.
Step-by-Step Closure Creation
Consider:
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
Step 1
createCounter() executes
JavaScript creates:
Execution Context
↓
count = 0
Step 2
The inner function is created.
It secretly stores
[[Environment]]
↓
count
Step 3
The outer function finishes.
Normally:
Variables
↓
Destroyed
But...
JavaScript detects that another function still references them.
Instead of deleting them...
It preserves them.
Step 4
Whenever we call
counter();
The function accesses
count
through its closure.
Why Variables Don't Disappear
Normally:
function test() {
let x = 10;
}
After execution
x
↓
Garbage Collection
Now compare
function test() {
let x = 10;
return function () {
console.log(x);
};
}
Now
Returned Function
↓
References x
↓
Garbage Collector
↓
Cannot delete x
Because it is still being used.
Closures and the Garbage Collector
JavaScript uses automatic memory management.
Objects are removed only when nothing references them.
Example
function createUser() {
let name = "Alice";
return function () {
console.log(name);
};
}
As long as the returned function exists...
The variable
name
cannot be removed.
Once the returned function becomes unreachable...
Everything becomes eligible for garbage collection.
Visualizing Memory
Before
const user = createUser();
Memory
Global
↓
user
↓
Closure
↓
name = Alice
Later
user = null;
Now
Nothing references Closure
Garbage Collector removes everything.
Memory Leaks
Closures themselves are not memory leaks.
The leak happens when we unintentionally keep references alive.
Bad Example
function createHugeArray() {
const data = new Array(1000000).fill("JavaScript");
return function () {
console.log(data.length);
};
}
Even if we only need the array length...
The entire array remains in memory.
Better
function createHugeArray() {
const length = new Array(1000000).fill("JS").length;
return function () {
return length;
};
}
Only a number is preserved.
Memory usage becomes much smaller.
Closures and Async Code
Closures become even more important with asynchronous JavaScript.
function fetchUser() {
let username = "John";
Promise.resolve().then(function () {
console.log(username);
});
}
fetchUser();
Output
John
The Promise callback remembers username.
Closures with async/await
async function loadUser() {
let id = 10;
await Promise.resolve();
console.log(id);
}
loadUser();
Even after the function pauses...
The variables remain available.
The execution context is suspended—not destroyed.
Closures and Higher-Order Functions
A Higher-Order Function either:
- accepts another function
- returns another function
Closures make this possible.
Example
function multiply(multiplier) {
return function (number) {
return multiplier * number;
};
}
Usage
const double = multiply(2);
const triple = multiply(3);
console.log(double(8));
console.log(triple(8));
Output
16
24
Each returned function owns its own closure.
Currying with Closures
Currying transforms
f(a, b)
into
f(a)(b)
Example
function add(a) {
return function (b) {
return a + b;
};
}
Usage
const addFive = add(5);
console.log(addFive(10));
Output
15
The value
a
is preserved by the closure.
Partial Application
Partial Application is closely related.
function calculateTax(rate) {
return function (price) {
return price * rate;
};
}
Usage
const egyptTax = calculateTax(0.14);
console.log(egyptTax(1000));
Output
140
Very common in Functional Programming.
Dependency Injection Using Closures
Instead of relying on global variables:
const logger = console.log;
Inject dependencies.
function createService(logger) {
return {
save(data) {
logger(data);
}
};
}
The logger becomes private.
Testing becomes much easier.
Common Interview Question
What will this print?
function outer() {
let x = 5;
return function () {
console.log(x);
};
}
const fn = outer();
fn();
Answer
5
Why?
Because the returned function closes over x.
Another Interview Question
function outer() {
let x = 10;
return {
increment() {
x++;
},
get() {
return x;
}
};
}
const obj = outer();
obj.increment();
obj.increment();
console.log(obj.get());
Output
12
Both methods share the same closure.
There is only one variable x.
Professional Uses of Closures
Closures power countless production-grade patterns, including:
- React Hooks (
useState,useMemo,useCallback) - Express Middleware
- Redux Middleware
- Authentication Wrappers
- Logging Utilities
- Rate Limiters
- Debounce & Throttle Functions
- Memoization
- Dependency Injection
- Function Factories
- Plugin Systems
- State Management Libraries
- Encapsulation and Data Privacy
Understanding closures deeply helps you understand why these tools work the way they do.
Best Practices
✅ Use closures for encapsulation
Keep internal state private and expose only the operations you need.
✅ Prefer let and const
Modern JavaScript avoids many closure-related bugs that were common with var.
✅ Avoid capturing unnecessary objects
A closure keeps everything it references alive.
Capture only the data you actually need.
✅ Be mindful of memory
Closures are lightweight, but retaining large objects unnecessarily can increase memory usage.
✅ Use closures intentionally
Closures are a powerful abstraction—not just a language feature. They allow you to build cleaner APIs, isolate state, and create reusable behavior without relying on global variables.
Final Thoughts
Closures are one of the defining features of JavaScript.
They are not a trick, nor are they limited to interview questions.
They are a fundamental mechanism that enables many modern JavaScript patterns—from simple callbacks to advanced frameworks like React and backend frameworks like Express.
Once you understand how closures interact with lexical environments, execution contexts, and the garbage collector, you'll be able to reason about JavaScript code more confidently, write safer abstractions, and recognize the patterns used throughout professional codebases.
In many ways, mastering closures is the point where JavaScript starts to feel less like a scripting language and more like a language designed for building sophisticated software systems.

Top comments (0)