Search code examples
javascriptclosureslexical-closures

Relationship between the LexicalEnviroment object and [[Enviroment]]


It is told that every block of code has a hidden object called LexicalEnviroment. That object contains a reference to the outer scope and an EnviromentRecord, which contains information about the current scope.

On the other hand, it is said that functions are capable of closure thanks to their [[Enviroment]] construct that "remembers where the function was defined".

I'm confused, what is the relationship between LexicalEnviroment object and [[Enviroment]]? Are they one and the same? Do only functions have [[Enviroment]] construct? Do they have a LexicalEnviroment object then?


Solution

  • tl;dr

    They are both instances of an Environment Record.

    LexicalEnvironment is only a component of execution contexts (functions can't have a LexicalEnvironment), and when invoking a function, a new LexicalEnvironment is created for the current context, with its [[OuterEnv]] field is being set to the Function [[Environment]] field.

    If it would be Javascript, I guess it would be:

    function handleInvokeFunction(func) {
        const localEnv = new EnvironmentRecord();
        localEnv.set('[[OuterEnv]]', func['[[Environment]]'])
        calleeContext.lexicalEnvironment = localEnv;
    }
    

    Disclaimer: I'm not an expert on the subject. I just want to give you an overall idea, while waiting for a real expert to chime in here.

    Environment Records

    Environment Records, for the record (pun intended), hold all information required for the function to be executed. For functions, for instance, they hold the variable declarations, and the this value. Of course, this is oversimplified [src].

    Environment Record is a specification type used to define the association of Identifiers to specific variables and functions.

    Each time such code is evaluated, a new Environment Record is created to record the identifier bindings that are created by that code.

    One interesting thing about Environment Records, is that they are responsible for allowing access to the parent variables, such in:

    // Environment Record "EnvA"
    const hello = "world";
    if (1) {
        // Environment Record "EnvB"
        console.log(hello);
    }
    
    // outputs: world
    

    That's because they have a field called [[OuterEnv]], which is pointing to the parent environment. So in the above example, the [[OuterEnv]] field of "EnvB" is set to "EnvA" [src].

    Every Environment Record has an [[OuterEnv]] field, which is either null or a reference to an outer Environment Record.

    Each time the runtime encounters a new block of code, it performs the following steps [src]:

    1. Create a new Environment Record.
    2. Set the [[OuterEnv]] field of that new environment to the old (currently active) environment.
    3. Return the new environment

    Execution Context

    In order to do this for all the blocks, an execution context stack is used, which is almost like a stack trace [src]. The difference is that instead of only pushing and poping on entering and exiting a function (like a stack trace would do), it will only change the topmost entry on entering or exiting blocks of code (like an if block).

    An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation.

    The execution context stack is used to track execution contexts.

    Execution contexts have a LexicalEnvironment component. It is needed to keep track of the variables in that specific block of code.

    LexicalEnvironment: Identifies the Environment Record used to resolve identifier references made by code within this execution context. [src]

    LexicalEnvironment is an Environment Record, so it has an [[OuterEnv]] field, which is what the runtime will be changing accordingly.

    LexicalEnvironment doesn't belong to function objects. It only belongs to a execution context.

    The running execution context represents the block of code the runtime is currently executing at that moment [src].

    The running execution context is always the top element of this stack.

    To expand on the above steps, when entering a new block of code, this is what would actually happen [src]:

    1. Create a new Environment Record with proper [[OuterEnv]] value (same steps as before).
    2. Use the new Environment Record as the running one.
    3. Evaluate all the lines inside the block.
    4. Restore to the previous Environment Record.
    5. Return the result and exit the block.

    Commenting on the previous example, this is what would happen:

    // This is Environment Record "EnvA".
    // The [[OuterEnv]] field for "EnvA" is null.
    // The running context LexicalEnvironment is "EnvA".
    
    const hello = "world";
    
    if (1) {
    
        // Found new block
    
        // Create a new Environment Record "EnvB".
    
        // Set the "EnvB" [[OuterEnv]] field to
        // the running context LexicalEnvironment.
        // In this case, its "EnvA".
    
        // Change the running context LexicalEnvironment to "EnvB".
        
        // Evaluate all lines in the body using the new 
        // running context LexicalEnvironment.
        // In this case, its "EnvB".
        
        console.log(hello);
        
        // Restore the previous running context LexicalEnvironment.
    
        // Return the result.
    }
    
    // The running context LexicalEnvironment is Environment Record "A".
    // Since the inner block restored before returning, it didn't change.
    

    [[Environment]]

    Still, no mention of functions yet. Which are different because functions can be executed outside the declared scope.

    That's where [[Environment]] appears.

    [[Environment]]: The Environment Record that the function was closed over. Used as the outer environment when evaluating the code of the function.

    When there is a function inside a block, the running LexicalEnvironment is stored as the [[Environment]] field of the function object [step 35][step 3][step 14].

    When calling that function, the [[Environment]] field is used as the [[OuterEnv]][step 10].

    It's like the function stores all the variables it had access inside [[Environment]], and when invoked, it can have access to them again using [[Environment]].

    The other difference with normal blocks, is that in this case, instead of changing the running execution context, a new one is created and pushed to the stack [creation in step 3][push in step 12][pop in step 8].

    Now, to try with a simple code:

    // This is Environment Record "EnvA".
    // The [[OuterEnv]] field for "EnvA" is null.
    // The running context LexicalEnvironment is "EnvA".
    
    const hello = "world";
    
    // Found a function, store the running context 
    // into its [[Environment]] field, and do nothing else.
    
    function foo() {
    
        // This block runs only after invoking bar().
        
        // Create a new executing context "calleeContext".
    
        // Create a new Environment Record "EnvB".
    
        // Set the "EnvB" [[OuterEnv]] field, to the value
        // stored inside [[Environment]]. In this case, its "EnvA".
    
        // Set the LexicalEnvironment of "calleeContext" to "EnvB".
    
        // Push "calleeContext" to the execution context stack.
        // That makes "calleeContext" the running execution context.
        
        // Evaluate all lines in the body
        // using "calleeContext" LexicalEnvironment.
        // In this case, its "EnvB".
    
        // If a function is found here, set its
        // [[Environment]] to "calleeContext" LexicalEnvironment.
        
        console.log(hello); // works because `hello` was in "EnvA"
        
        // Pop "calleeContext" from the execution context stack.
        // "calleeContext" is no longer the running execution context.
        
        // Return the result.
    }
    
    const bar = foo;
    bar();
    
    // The [[Environment]] of `bar` is still "EnvA".
    // The running context LexicalEnvironment is still "EnvA".
    

    Since the example is calling the function in the same environment that it was declared, it is not actually using "closures", but you might get the idea already.

    In conclusion

    Although both [[Environment]] and LexicalEnvironment are Environment Records, they are using for different things.

    [[Environment]] holds the LexicalEnvironment where the function was declared.

    LexicalEnvironment is a component of execution contexts, which stores information about the variables in that particular block of code.