JS Tip of the Day: Closure Scopes Limit Closure Variables

Closure Scopes Limit Closure Variables
Level: Advanced

Closures are functions associated with a lexical environment. Lexical environments store variables in a scope. They also store a reference to their parent environment creating an environment chain that goes all the way back up to the global environment. A closure is able to access variables in outer scopes through this chain of environments starting with the one that its been associated with.

While this is great for closures, its not always great for memory. A single closure function could be holding on to a lot of information depending on how many environments are in its chain. Chances are the function doesn’t even care about most of the information in most of those environments - if any at all. The good news is, modern JavaScript runtimes optimize a closure’s environment chain by removing anything it doesn’t need up to, but not including global (everyone gets global).

This is not something you’ll normally notice. Afterall, why does it matter if a function doesn’t have access to something it doesn’t use? But it is something that can come up when debugging. If you’re ever trying to access a variable from some parent scope inside a closure that the closure isn’t using, you may notice that it may appear to not exist. This would be because the variable was optimized away.

function createValueReporter () {
    let id = 'value';
    let value = 1;
    return function () {
        debugger; // Can't see `id` here
        return value;
    }
}

let valueReporter = createValueReporter()
console.log(valueReporter()); // 1

The closure returned by createValueReporter simply returns the value in the value variable. It’s able to do this just fine because, as you’d expect, being the closure that it is, it can access value from it’s parent scope. But if you’re debugging, and while in this closure function and you attempt to access and determine the value of id, you’ll notice that it doesn’t exist. This is because the only variables retained by the environment were those used by the closure. Since id was not being used, it was removed as part of a closure optimization.

One thing to keep in mind is that environments can be shared. When a closure is associated with an environment, it’s not a copy of the original environment, it’s the same environment that another closure in that same scope would also be using. This means if another closure referred to other variables, each of those closures would have an environment - the same environment - with all of the variables needed to accommodate each closure. We can see this with a modification of the previous example.

function createValueReporter () {
    let id = 'value';
    let value = 1;
    return {
        idReporter () {
            return id;
        },
        valueReporter () {
            debugger; // Now able to see `id` here
            return value;
        }
    };
}

let { idReporter, valueReporter } = createValueReporter();
console.log(idReporter()); // 'value'
console.log(valueReporter()); // 1

Now that idReporter is using the id variable, the environment will also on to that variable. And since idReporter and valueReporter share the same environment, id will be accessible from within the valueReporter function as well, despite the fact that valueReporter itself isn’t referring to it.

As a bonus, while debuggers will usually make it pretty easy to see what variables are available or in scope while debugging, you may be interested to know that you may also be able to see what variables are in scope for a closure without debugging. For example the Chrome console will show this through a hidden property when the function is logged through console.dir(). This [[Scopes]] property (made to look like an internal slot but it is not one officially defined in the spec) will list out the environment chain as an array of objects showing which scopes and variables within those scopes are available to that closure function.

{
    let myVar = 'block';
    function myFunc () {
        return myVar;
    }
    console.dir(myFunc);
    /*
    ...
    [[Scopes]]: Scopes[3]
        0: Block {myVar: "block"}
        1: Script {...}
        2: Global {...}
    */
}

Note: The “Script” scope in that list represents global declarations (global let, const, and class).

More info:


More tips: JavaScript Tips of the Day