DEV Community

Cover image for JavaScript Closures — Part 3 (Engine Internals, Memory Management & Professional Patterns)
Abanoub Kerols
Abanoub Kerols

Posted on

JavaScript Closures — Part 3 (Engine Internals, Memory Management & Professional Patterns)

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
Enter fullscreen mode Exit fullscreen mode

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;

}
Enter fullscreen mode Exit fullscreen mode

Internally, JavaScript stores something conceptually like this:

Lexical Environment

name → "John"

age → 30
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Example:

function user() {

    let firstName = "John";

    let lastName = "Doe";

}
Enter fullscreen mode Exit fullscreen mode

Environment Record

firstName → John

lastName → Doe
Enter fullscreen mode Exit fullscreen mode

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);

    }

}
Enter fullscreen mode Exit fullscreen mode

Internally

Global Environment

↓

country

↓

Outer Environment

↓

city

↓

Inner Environment
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Example

function outer() {

    let score = 50;

    return function () {

        console.log(score);

    };

}
Enter fullscreen mode Exit fullscreen mode

When the inner function is created...

JavaScript stores something like:

Inner Function

↓

[[Environment]]

↓

Outer Lexical Environment
Enter fullscreen mode Exit fullscreen mode

That hidden reference is what makes closures possible.


Step-by-Step Closure Creation

Consider:

function createCounter() {

    let count = 0;

    return function () {

        count++;

        return count;

    };

}
Enter fullscreen mode Exit fullscreen mode

Step 1

createCounter() executes
Enter fullscreen mode Exit fullscreen mode

JavaScript creates:

Execution Context

↓

count = 0
Enter fullscreen mode Exit fullscreen mode

Step 2

The inner function is created.

It secretly stores

[[Environment]]

↓

count
Enter fullscreen mode Exit fullscreen mode

Step 3

The outer function finishes.

Normally:

Variables

↓

Destroyed
Enter fullscreen mode Exit fullscreen mode

But...

JavaScript detects that another function still references them.

Instead of deleting them...

It preserves them.


Step 4

Whenever we call

counter();
Enter fullscreen mode Exit fullscreen mode

The function accesses

count
Enter fullscreen mode Exit fullscreen mode

through its closure.


Why Variables Don't Disappear

Normally:

function test() {

    let x = 10;

}
Enter fullscreen mode Exit fullscreen mode

After execution

x

↓

Garbage Collection
Enter fullscreen mode Exit fullscreen mode

Now compare

function test() {

    let x = 10;

    return function () {

        console.log(x);

    };

}
Enter fullscreen mode Exit fullscreen mode

Now

Returned Function

↓

References x

↓

Garbage Collector

↓

Cannot delete x
Enter fullscreen mode Exit fullscreen mode

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);

    };

}
Enter fullscreen mode Exit fullscreen mode

As long as the returned function exists...

The variable

name
Enter fullscreen mode Exit fullscreen mode

cannot be removed.

Once the returned function becomes unreachable...

Everything becomes eligible for garbage collection.


Visualizing Memory

Before

const user = createUser();
Enter fullscreen mode Exit fullscreen mode

Memory

Global

↓

user

↓

Closure

↓

name = Alice
Enter fullscreen mode Exit fullscreen mode

Later

user = null;
Enter fullscreen mode Exit fullscreen mode

Now

Nothing references Closure
Enter fullscreen mode Exit fullscreen mode

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);

    };

}
Enter fullscreen mode Exit fullscreen mode

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;

    };

}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Output

John
Enter fullscreen mode Exit fullscreen mode

The Promise callback remembers username.


Closures with async/await

async function loadUser() {

    let id = 10;

    await Promise.resolve();

    console.log(id);

}

loadUser();
Enter fullscreen mode Exit fullscreen mode

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;

    };

}
Enter fullscreen mode Exit fullscreen mode

Usage

const double = multiply(2);

const triple = multiply(3);

console.log(double(8));

console.log(triple(8));
Enter fullscreen mode Exit fullscreen mode

Output

16

24
Enter fullscreen mode Exit fullscreen mode

Each returned function owns its own closure.


Currying with Closures

Currying transforms

f(a, b)
Enter fullscreen mode Exit fullscreen mode

into

f(a)(b)
Enter fullscreen mode Exit fullscreen mode

Example

function add(a) {

    return function (b) {

        return a + b;

    };

}
Enter fullscreen mode Exit fullscreen mode

Usage

const addFive = add(5);

console.log(addFive(10));
Enter fullscreen mode Exit fullscreen mode

Output

15
Enter fullscreen mode Exit fullscreen mode

The value

a
Enter fullscreen mode Exit fullscreen mode

is preserved by the closure.


Partial Application

Partial Application is closely related.

function calculateTax(rate) {

    return function (price) {

        return price * rate;

    };

}
Enter fullscreen mode Exit fullscreen mode

Usage

const egyptTax = calculateTax(0.14);

console.log(egyptTax(1000));
Enter fullscreen mode Exit fullscreen mode

Output

140
Enter fullscreen mode Exit fullscreen mode

Very common in Functional Programming.


Dependency Injection Using Closures

Instead of relying on global variables:

const logger = console.log;
Enter fullscreen mode Exit fullscreen mode

Inject dependencies.

function createService(logger) {

    return {

        save(data) {

            logger(data);

        }

    };

}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Answer

5
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

Output

12
Enter fullscreen mode Exit fullscreen mode

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)