How to create a closure in JavaScript

How to create a closure in JavaScript

In the universe of compiled languages like C or Pascal, the life of a local variable is brutally short and predictable. When a function is invoked, the system carves out a chunk of memory on the call stack, a structure perfectly named for its last-in, first-out behavior. This chunk, the stack frame, becomes the temporary home for the function’s parameters, its return address, and all the variables declared within its body. The moment the function completes its work and executes its final return, the stack frame is obliterated. Popped off the stack, its memory is reclaimed, and every local variable within it ceases to exist as if it had never been. This is a clean, efficient, and deterministic model. The variables are ephemeral, their existence tied directly to the execution context of their parent function.

JavaScript, at first glance, appears to play by these same rules. Consider a function that performs a simple calculation:

function calculateTotal(basePrice) {
    const salesTax = 1.08; // Lives inside calculateTotal
    let finalPrice = basePrice * salesTax;
    console.log(The final price is ${finalPrice}.);
}

calculateTotal(100);

When calculateTotal(100) is called, a scope is created for its execution. The variables salesTax and finalPrice spring into being. The function runs, logs “The final price is 108.”, and returns. At that instant, our C-based intuition tells us that the scope is destroyed. The variables salesTax and finalPrice are gone, their memory released back to the system. They lived and died inside that single function call. So far, so good. The model holds.

But JavaScript has a capability that fundamentally shatters this simple stack-based reality. Functions in JavaScript are first-class citizens, meaning they can be treated like any other value: assigned to variables, passed as arguments, and, most critically for our purposes, returned from other functions. This is where the fabric of our understanding begins to tear. Let’s build a function factory, a function that creates and returns another function.

function createMultiplier(factor) {
    // 'factor' is a local variable, born when createMultiplier is called.
    
    return function(number) {
        // This inner, anonymous function USES the 'factor' variable.
        return number * factor;
    };
}

const double = createMultiplier(2);

Let’s trace what just happened with our stack-based mental model. We called createMultiplier(2). A new execution scope was created. Inside this scope, the local variable factor was created and assigned the value 2. Then, the function did its work: it created a new, anonymous function and returned it. The createMultiplier function has now completed its execution. It has returned. According to everything we know, its stack frame should be popped, and its local variable, factor, should be annihilated. Gone. Destroyed. The variable double now holds the anonymous function that was returned, but the environment in which that function was created is history.

So what should happen when we try to execute the function now stored in double?

let result = double(10); // What is 'factor'? It should be gone.

console.log(result); // Outputs: 20

It works. It produces 20. This is deeply strange. The function we’re calling as double(10) executes its code, return number * factor;. It has access to its own local variable number (which is 10), but how, in the name of all that is computable, does it know the value of factor? The parent function, createMultiplier, finished running long ago. Its scope should have been dismantled. The variable factor should not exist anywhere in memory. Yet, it does. It was not destroyed. It has been preserved, somehow, somewhere, waiting for the inner function to be called. The simple model of a function’s variables dying when the function returns is clearly wrong, or at least incomplete. The JavaScript engine is not merely using a transient stack for its local variables. When it compiled the inner function, it saw that the function referenced a variable, factor, from its containing environment. Because of this reference, the engine ensured that this variable would survive the death of its original scope. It has been enclosed-or closed over-by the inner function, which now carries this piece of its birth environment with it, like a genetic inheritance. The variable didn’t die because the function returned from createMultiplier maintains a persistent link to it, keeping it alive in a memory space that is not the ephemeral call stack.

Forging the scope chain

This preservation is not magic; it is a fundamental mechanism of the language’s execution model. When a function is defined in JavaScript, it’s more than just a bundle of executable code. The engine creates a function object that contains not only the code but also a hidden, internal property-let’s call it [[Environment]]-which is a reference to the lexical environment (the scope) in which the function was created. This is a static link, forged at the moment of creation, based entirely on where the function sits in the source code. It is a birthright, an unbreakable connection to its place of origin.

When we later invoke this function, say by calling double(10), a new execution context is created. This context gets its own new environment record to store its local variables (in this case, number with the value 10). But critically, the context also sets its outer environment reference to the value stored in the function’s [[Environment]] property. This creates a linked list of environments, starting from the currently executing function’s scope and moving outwards. This linked list is the scope chain.

So, when the line return number * factor; is executed inside double, the engine needs to find the values for number and factor. It first looks in the current, innermost environment record. It finds number immediately. Success. Next, it looks for factor. It’s not in the current environment. Instead of giving up, the engine follows the outer environment reference-the link in the scope chain-to the next scope out, which is the environment of the createMultiplier(2) call. It searches that environment record. And there it is: factor, with its value of 2. The variable is resolved, the multiplication happens, and 20 is returned. The scope of createMultiplier was not on the call stack to be popped; it was allocated on the memory heap, and the garbage collector was prevented from reclaiming it because the inner function’s [[Environment]] property held a live reference to it.

This pattern reveals its true power when multiple functions are bound to the same environment. Consider a counter:

function createCounter() {
    let count = 0; // Scope of createCounter

    return {
        increment: function() { // Inner function 1
            count++;
            return count;
        },
        getValue: function() { // Inner function 2
            return count;
        }
    };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // Outputs: 2

Here, createCounter returns an object with two methods, increment and getValue. Both of these functions were defined within the scope of createCounter, so both of them receive an [[Environment]] reference pointing to that same, single lexical environment. They share access to the same count variable. When counter.increment() is called, it follows its scope chain, finds count, and modifies it. When counter.getValue() is called, it follows its own scope chain to that very same environment and reads the current value of count. They are bound to the same living, breathing piece of state, which persists across multiple, independent function calls. The createCounter function has long since finished, but its variable count lives on in this heap-allocated environment, a private little universe accessible only by the functions that were forged within it. The scope chain isn’t just a lookup path; it’s a lifeline that keeps variables alive long after their parent function has vanished from the call stack.

Stateful functions from simple parts

What we have constructed with createCounter is far more significant than a simple counting device. We have, using nothing but nested functions, synthesized an object with truly private state. The variable count is the soul of our counter, its internal memory. Yet, no code outside of the returned increment and getValue methods can ever touch it, read it, or even know of its existence. It is perfectly encapsulated. Compare this to the more conventional object literal approach:

const typicalCounter = {
    count: 0,
    increment: function() { this.count++; },
    getValue: function() { return this.count; }
};

// The state is exposed and vulnerable
typicalCounter.count = 999; // The contract is broken
typicalCounter.increment(); // Now results in 1000, not 1
console.log(typicalCounter.getValue()); // Outputs: 1000

In this common pattern, the count property is a public field on the object. Any piece of code with a reference to typicalCounter can directly manipulate it, bypassing the intended logic of the increment method. The integrity of the counter’s state is fragile, dependent on the discipline of every programmer who interacts with it. The closure-based approach, however, enforces this discipline at the language level. The privacy of count is not a convention; it is a physical boundary created by the scope chain. The only gateways to that variable are the functions we explicitly chose to return, which now act as a hardened public API for a protected internal state.

This technique, often called the Module Pattern, is a foundational method for building robust, encapsulated components in JavaScript. We can forge complex systems with private data and behaviors, exposing only a controlled interface to the outside world. Let’s build a slightly more sophisticated machine: a simple key-value cache that logs its operations.

function createLoggerCache() {
    // Private state, completely inaccessible from the outside
    const cache = {};
    const log = [];
    let hits = 0;
    let misses = 0;

    function getTimestamp() {
        return new Date().toISOString();
    }

    // The public interface, composed of functions that close over the private state
    return {
        add: function(key, value) {
            if (cache.hasOwnProperty(key)) {
                log.push([${getTimestamp()}] OVERWRITE key '${key}');
            } else {
                log.push([${getTimestamp()}] ADD key '${key}');
            }
            cache[key] = value;
        },

        get: function(key) {
            if (cache.hasOwnProperty(key)) {
                hits++;
                log.push([${getTimestamp()}] HIT key '${key}');
                return cache[key];
            } else {
                misses++;
                log.push([${getTimestamp()}] MISS key '${key}');
                return null;
            }
        },

        getStats: function() {
            return {
                hits: hits,
                misses: misses,
                entryCount: Object.keys(cache).length
            };
        },
        
        getLog: function() {
            return [...log]; // Return a copy to prevent external modification
        }
    };
}

const myCache = createLoggerCache();
myCache.add('user:1', { name: 'Alice' });
myCache.add('user:2', { name: 'Bob' });
myCache.get('user:1');
myCache.get('user:3'); // This will be a miss

console.log(myCache.getStats());
// Outputs: { hits: 1, misses: 1, entryCount: 2 }

Once createLoggerCache() has executed, the variables cache, log, hits, and misses are sealed away. They exist on the heap in a persistent lexical environment, an environment shared by the four functions returned in the public API object. The only way to interact with the cache is through add and get. The only way to see the statistics is through getStats. You cannot, for example, directly clear the log array from the outside (myCache.log.length = 0 would fail because myCache.log is undefined). We even took the extra step in getLog to return a *copy* of the log array, ensuring that the caller can’t mutate our internal state by manipulating the returned array. Each call to createLoggerCache would produce a completely independent instance, with its own private cache, log, and counters. We are not just creating functions; we are manufacturing self-contained, stateful components from the simple raw materials of functions and lexical scope. This isn’t an esoteric trick; it’s a direct consequence of how the language is designed to work, turning a potential point of confusion into a powerful tool for software architecture. The closure is the membrane that separates the internal machinery from the outside world.

There is no such thing as a free variable

In the formalisms of computer science, a variable like factor inside our returned multiplier function is known as a “free variable.” The term signifies that the variable is not “bound” within the function’s own immediate scope-it is neither a local variable declared with let, const, or var, nor is it a parameter. It’s an outsider. Yet in the concrete reality of the JavaScript engine, this term is deeply misleading. The variable is not free in any sense of being untethered or unattached. It is captured. It is owned. Its identity is immutably fixed by a simple, powerful rule: lexical scoping.

The resolution of these seemingly free variables is determined entirely by the physical location of the function’s definition in the source code, not by the dynamic context in which the function is eventually executed. This is the principle of lexical (or static) scope. The alternative, dynamic scope, where variables are resolved by searching backward through the call stack, is a path JavaScript explicitly rejects. The implications of this choice are profound and are the very reason closures are reliable. Consider this scenario, which pits lexical and dynamic scope against each other:

let name = "Global";

function showName() {
    // 'name' is a "free variable" here. Where does it come from?
    console.log(name);
}

function runWithDifferentName(fn) {
    let name = "Local to runWithDifferentName";
    fn(); // We invoke the function here.
}

runWithDifferentName(showName); // What will this log?

If JavaScript were dynamically scoped, the chain of execution would determine the outcome. The call to fn() (which is showName) happens inside runWithDifferentName. When showName looks for name, a dynamically scoped language would search the scope of its caller, find the local name variable inside runWithDifferentName, and log “Local to runWithDifferentName”. The behavior of showName would change depending on who called it.

But that is not what happens. The code, when run, will unequivocally log “Global”. The reason is lexical scope. When the showName function is first parsed, the engine establishes its scope. It sees the reference to name. It looks for a local declaration and finds none. It then looks one level up in its lexical environment-the environment where it was written-which is the global scope. It finds the global name variable and forges a permanent link. The function showName, and the closure created from it, will forever resolve its free variable name by looking in the global scope, regardless of where, when, or how it is called. When we pass it into runWithDifferentName, it carries its lexical environment with it. The local name variable declared inside runWithDifferentName is completely invisible to showName; it is not part of the scope chain that was baked in at definition time.

There is, therefore, no such thing as a truly free variable in JavaScript. Every variable is bound to a scope in a predictable chain. The function does not float freely, adapting to the environment of its caller. It is an artifact of its origin, rigidly tethered to the lexical scope in which it was born. The scope chain is not a dynamic path forged at runtime based on the call stack; it is a static, unchangeable map back to a function’s birthplace. This determinism is the bedrock upon which all closure-based patterns are built.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *