Closures: What Actually Happens Inside the Engine? — Under the Hood of JavaScript
Most JavaScript articles explain closures like this:

Closures: What Actually Happens Inside the Engine? — Under the Hood of JavaScript
Most JavaScript articles explain closures like this:
A closure is a function bundled together with its lexical environment.
While this definition comes from MDN and is correct, it doesn’t answer the questions experienced developers usually have:
- Why does JavaScript need closures in the first place?
- Why aren’t local variables destroyed after a function returns?
- Are variables copied into a closure?
- Where are captured variables stored — in the stack or the heap?
- Does every nested function create a closure?
- How does the garbage collector know when a closure can be removed?
If you’ve ever prepared for senior JavaScript interviews, you’ve probably noticed these questions come up far more often than the textbook definition.
In this article, we’ll build closures from the ground up by understanding how the JavaScript execution model works.
The Problem JavaScript Needs to Solve
Consider this code:
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const increment = outer();
increment();
increment();
increment();
Output:
1
2
3
At first glance, this seems normal.
But think about what should happen.
When outer() finishes executing, its execution context is removed from the call stack.
Normally, local variables disappear when a function returns.
So why does count still exist?
This is the problem closures solve.
Understanding the Execution Context
Whenever JavaScript executes a function, the specification creates a new Execution Context.
Conceptually, an execution context contains:
- Lexical Environment
- Variable Environment
- This Binding
For most modern JavaScript code, the Lexical Environment is what matters most when discussing closures.
What Is a Lexical Environment?
A Lexical Environment is a specification concept defined by ECMAScript.
It is not a JavaScript object you can access.
It contains two important parts:
Lexical Environment
│
├── Environment Record
└── Outer Environment Reference
The Environment Record stores variable bindings.
For example:
function greet(name) {
const message = "Hello";
}
Conceptually, the Environment Record contains something similar to:
Environment Record
name -> "Alice"
message -> "Hello"
Again, this is not the actual memory layout inside a JavaScript engine. It is the abstract model used by the ECMAScript specification.
The Scope Chain
Every Lexical Environment has a reference to its outer environment.
Imagine:
const country = "Pakistan";
function outer() {
const city = "Lahore";
function inner() {
console.log(country);
console.log(city);
}
inner();
}
outer();
Conceptually:
Global Environment
│
▼
Outer Environment
│
▼
Inner Environment
When JavaScript looks for a variable, it searches:
- Current Environment Record
- Outer Environment
- Continue outward
- Stop at the global environment
This is called the scope chain.
Now Let’s Build a Closure
Consider:
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}
When outer() executes:
count = 0
The returned function references count.
The important detail is that the function does not receive a copy of the variable.
Instead, it retains access to the same lexical environment where count was originally declared.
This is why every call updates the same variable rather than creating a new one.
Variables Are Not Copied
A common misconception is that closures copy variables.
If that were true:
let count = 0;
would become
count = 0
inside the returned function forever.
But that’s clearly false.
1
2
3
shows that the same binding is being updated.
Closures preserve access to the original binding rather than copying the value.
Why Isn’t Memory Released?
Normally, once a function returns, its execution context is removed from the call stack.
However, removing an execution context from the stack does not automatically mean every value associated with it becomes unreachable.
If another reachable object still references the data, the garbage collector must keep it alive.
The returned function still needs access to count, so the captured binding remains reachable.
Only after nothing can reach that function anymore can the associated data become eligible for garbage collection.
Does Every Nested Function Create a Closure?
No.
This is one of the biggest misconceptions.
Consider:
function outer() {
const value = 10;
function inner() {
console.log("Hello");
}
inner();
}
Here, inner never accesses value.
From the language perspective, there is no need for inner to preserve access to outer's local bindings after outer returns.
Modern JavaScript engines are also free to optimize cases like this internally, as long as observable JavaScript behavior remains identical.
Closures Are a Language Feature, Not a V8 Feature
Closures are part of the ECMAScript language specification.
Every compliant JavaScript engine implements closure semantics.
However, the internal representation is implementation-specific.
For example:
- V8
- SpiderMonkey
- JavaScriptCore
may use different internal data structures while exposing the same JavaScript behavior.
This distinction is important:
- Lexical Environment and Environment Record are specification concepts.
- The concrete runtime structures used by an engine are implementation details.
Interview Question
Why does this print 3 three times?
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
Many people answer:
Because
varis function-scoped.
That’s only part of the explanation.
All three callbacks close over the same binding for i. By the time the callbacks execute, the loop has completed and that binding holds the value 3.
Replacing var with let changes the behavior because the language creates a new binding for each loop iteration, so each callback captures a different binding.
We’ll explore this mechanism in detail in the next article.
Key Takeaways
- Closures exist because functions may need access to variables after the outer function has returned.
- Variables are not copied into a closure.
- Closures preserve access to the original lexical environment.
- Lexical Environments and Environment Records are specification concepts defined by ECMAScript.
- JavaScript engines are free to implement these concepts differently internally.
- A closure and the variables it captures remain alive only while they are still reachable.
In the next article, we’ll go one level deeper and examine how JavaScript engines represent lexical environments internally, how closures interact with the garbage collector, and what optimizations modern engines perform to reduce their memory overhead.
메타데이터
- post_id
- db4f255a55da
- slug
- closures-what-actually-happens-inside-the-engine-under-the-hood-of-javascript-db4f255a55da
- url
- https://medium.com/@zubairasim7/closures-what-actually-happens-inside-the-engine-under-the-hood-of-javascript-db4f255a55da
- canonical_url
- https://medium.com/@zubairasim7/closures-what-actually-happens-inside-the-engine-under-the-hood-of-javascript-db4f255a55da
- author_url
- https://medium.com/@zubairasim7
- status
- ok
- fetched_at
- 2026-06-22 19:40:15