Unveiling JavaScript Closures: How Scope Chains and Memory Work Under the Hood

This article demystifies JavaScript closures by explaining when they are created, how the scope chain and lexical environments operate, why closures persist in memory, and how nested functions and the garbage collector interact, illustrated with clear code examples and diagrams.

21CTO
21CTO
21CTO
Unveiling JavaScript Closures: How Scope Chains and Memory Work Under the Hood

I've been studying JavaScript closures for a while. I knew how to use them but never fully understood how they work internally, so what exactly is a closure?

Wikipedia's definition is not very helpful. When is a closure created, when is it destroyed, and how is it implemented?

"use strict";

Below is a classic closure example that returns an object with an inc method that increments a hidden variable:

var myClosure = (function outerFunction() {
    var hidden = 1;
    return {
        inc: function innerFunction() {
            return hidden++;
        }
    };
}());

myClosure.inc(); // returns 1
myClosure.inc(); // returns 2
myClosure.inc(); // returns 3

The code demonstrates that each call to inc sees the updated hidden value because the inner function retains a reference to the outer function's scope object.

Scope Chain and Lexical Environment

When JavaScript runs, it stores local variables in a scope object (also called a LexicalEnvironment ). Each function call creates its own scope object, which lives on the heap rather than the stack, so it can survive after the function returns.

Scope objects form a parent‑child relationship. When the interpreter looks up a variable, it searches the current scope object; if the variable is not found, it walks up the scope chain to the parent scope, continuing until it reaches the global object or throws a ReferenceError.

The top of the chain is the global object. Code executed in the global environment has a scope chain that contains only the global object.

In the global environment we create two variables:

var foo = 1;
var bar = 2;

These variables are stored in the global scope object.

Non‑nested Functions

Consider a simple function that accesses variables from the outer scope:

function myFunc() {
    var a = 1;
    var b = 2;
    var foo = 3;
    console.log("inside myFunc");
}
console.log("outside");
myFunc();

When myFunc is defined, its identifier is added to the global scope object, and the function object receives an internal [[scope]] property that points to the global scope object. When the function is called, a new scope object is created for its local variables ( a, b, foo) whose parent is the global scope.

Variable lookup follows the chain: a is found in the function's own scope, foo is also found there, while bar is found in the global scope.

Nested Functions and Closures

When a function returns another function, the inner function retains a reference to the outer function's scope object via its [[scope]]. As long as the returned function is referenced, the outer scope object cannot be garbage‑collected.

function myFunc() {
    return innerFunc;
    function innerFunc() { /* ... */ }
}
var inner = myFunc(); // <code>inner</code> holds a closure over <code>myFunc</code>'s scope

Because the inner function still references the outer scope, the variables inside that scope remain alive.

Practical Closure Example: Counter

A common pattern is a factory that creates a private counter:

function createCounter(initial) {
    var counter = initial;
    function increment(value) { counter += value; }
    function get() { return counter; }
    return { increment: increment, get: get };
}
var myCounter = createCounter(100);
console.log(myCounter.get()); // 100
myCounter.increment(5);
console.log(myCounter.get()); // 105

When createCounter(100) runs, a new scope object is created containing counter and initial. The returned object holds references to increment and get, both of which keep a reference to that scope object, so counter persists even after the factory function returns.

Calling myCounter.get() creates a temporary scope for the call; it looks for counter in its own (empty) scope, then finds it in the parent scope created by createCounter. The same happens for increment, which also receives the argument value in its own call‑scope.

Multiple counters illustrate that each closure has its own independent scope:

var c1 = createCounter(100);
var c2 = createCounter(200);
console.log(c1.get()); // 100
console.log(c2.get()); // 200
c1.increment(3);
c2.increment(5);
console.log(c1.get()); // 103
console.log(c2.get()); // 205

Even though increment and get have identical source code, their [[scope]] points to different lexical environments, so they operate on different private counter variables.

Scope Chain and this

The value of this is not stored in the scope chain; it is determined by how a function is called, not by lexical scope.

Summary

What is a closure? A closure is an object that contains a function and a reference to its lexical environment; effectively every JavaScript object is a closure.

When is a closure created? As soon as a function is defined.

When is a closure destroyed? When no other object holds a reference to its lexical environment.

For further reading see the original article "How do JavaScript closures work under the hood" by Wu Di.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Memory ManagementfunctionsclosureScope Chain
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.