DEV Community

Cover image for Lexical Environment-the hidden part to understand Closures
Amandeep Singh
Amandeep Singh

Posted on

Lexical Environment-the hidden part to understand Closures

Closures can be a daunting concept when you are new to JavaScript world. Scouring the internet will give you tons of definition about what closure is. But I have felt that mostly these definitions are vague and don’t explain the fundamental cause of their existence.

Today we will try to demystify some of these concepts which are part of the ECMAScript 262 specs, including Execution Context, Lexical Environment, and Identifier Resolution. Additionally, we will learn that because of these mechanisms, all functions in ECMAScript are closures.

I will explain the terminology first and then show you some code examples explaining how all these pieces work together. This will help to solidify your understanding.

Execution Context

JavaScript interpreter creates a new context whenever it’s about to execute a function or script we’ve written. Every script/code starts with an execution context called a global execution context. And every time we call a function, a new execution context is created and is put on top of the execution stack. The same pattern follows when you call the nested function which calls another nested function:

alt text

Let’s see what happens when our code is executed as shown in the picture above:

  • A global execution context is created and placed at the bottom of the execution stack.
  • When the bar is invoked, a new bar execution context is created and is put on top of the global execution context.
  • As, bar calls to a nested function foo, a new foo execution context is created and is placed on top of the bar execution context.
  • When foo returns, its context is popped out of the stack and flow returns to the bar context.
  • Once bar execution is finished, the flow returns back to the global context and finally, the stack is emptied.

Execution stack works on a LIFO data structure way. It waits for the topmost execution context to return before executing the context below.

Conceptually, Execution context has a structure which looks like the following:

  // Execution context in ES5
ExecutionContext = {
  ThisBinding: <this value>,
  VariableEnvironment: { ... },
  LexicalEnvironment: { ... }
}
Enter fullscreen mode Exit fullscreen mode

Don’t worry if structure looks intimidating. We will look at these components shortly. The key point to remember is that every call to execution context has two stages: Creation Stage and Execution Stage. Creation Stage is when the context is created but not invoked yet.

A few things happen in the creation stage:

  • VariableEnvironment component is used for the initial storage for the variables, arguments and function declarations. The var declared variables are initialized with the value of undefined.
  • The value of This is determined.
  • LexicalEnvironment is just the copy of VariableEnvironment at this stage.

Upon execution stage:

  • Values are assigned.
  • LexicalEnvironment is used to resolve the bindings.

Now, let’s try to understand what is a lexical environment.

Lexical Environment

According to ECMAScript specification 262 (8.1):

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code.

Let’s try to simplify a few things here. A lexical environment consists of two main components: the environment record and a reference to the outer (parent) lexical environment:

var x = 10;

function foo(){
  var y = 20;
 console.log(x+y); // 30
}

// Environment technically consists of two main components:
// environmentRecord, and a reference to the outer environment

// Environment of the global context
globalEnvironment = {
  environmentRecord: {
    // built-ins
    // our bindings:
    x: 10
  },
  outer: null // no parent environment
};

// Environment of the "foo" function
fooEnvironment = {
  environmentRecord: {
    y: 20
  },

  outer: globalEnvironment
};
Enter fullscreen mode Exit fullscreen mode

Visually it will look like this:

alt text

As you can see when trying to resolve the identifier “y” in the foo context, the outer environment (global) is reached out. This process is called identifier resolution and happens on running execution context.

Now, armed with this knowledge of Environments, let’s get back to the structure of Execution context and see what’s happening there:

  • VariableEnvironment: Its environmentRecord is used for the initial storage for the variables, arguments and function declarations, which later is filled on entering the context activation stage.
function foo(a) {
  var b = 20;
}
foo(10);

// The VariableEnvironment component of the foo function
//context at creation stage
fooContext.VariableEnvironment = {
  environmentRecord: {
    arguments: { 0: 10, length: 1, callee: foo },
    a: 10,
    b: undefined
  },
  outer: globalEnvironment
};

// After the execution stage, the VE envRec
// table is filled in with the value
fooContext.VariableEnvironment = {
  environmentRecord: {
    arguments: { 0: 10, length: 1, callee: foo },
    a: 10,
    b: 20
  },
  outer: globalEnvironment
};
Enter fullscreen mode Exit fullscreen mode
  • LexicalEnvironment: Initially, it’s just a copy of the VariableEnvironment. On the running context, it is used to determine the binding of an identifier appearing in the context.

Both VE and LE by their nature are lexical environments, i.e both statically(at creation stage) captures the outer bindings for inner functions created in the context. This mechanism gives rise to closures.

Capturing the outer binding statically for inner functions give rise to the formation of closures.

Identifier Resolution aka Scope chain lookup

Before understanding the closure, let’s understand how the scope chain is created in our execution context. As we saw earlier, each execution context has LexicalEnvironment which is used for identifier resolution. All the local bindings for the context are stored in the environment record table. If identifiers are not resolved in the current environmentRecord, the resolution process will continue to the outer (parent) environment record table. This pattern will continue until the identifier is resolved. If not found, a ReferenceError is thrown.

This is very similar to the prototype lookup chain. Now, the key to remember here is that LexicalEnvironment captures the outer binding lexically (statically) on context creation stage and used as it is on the running context (execution stage).

Closures

As we saw in the previous section that upon function creation stage, statically saving of outer binding in the LexicalEnvironment of inner context gives rise to closures regardless of whether a function will be activated later or not. Let see that in an example:

Example 1:

var a = 10;
function foo(){
  console.log(a);
};
function bar(){
  var a = 20;
  foo();
};
bar(); // will print "10"
Enter fullscreen mode Exit fullscreen mode

The LexicalEnvironment of foo captures the binding “a” at creation time, which was 10. So, when foo is invoked later (at execution stage), the “a” identifier is resolved with a value of 10 but not 20.

Conceptually, the identifier resolution process will look something like this:

// check for binding "a" in the env record of "foo"
-- foo.[[LexicalEnvironment]].[[Record]] --> not found

// if not found, check for its outer environment


--- global[[LexicalEnvironment]][[Record]] --> found 10
// resolve the identifier with a value of 1
Enter fullscreen mode Exit fullscreen mode

alt text

Example 2

function outer() {
 let id = 1;

 // creating a function would statically captures
 // the value of 'id' here
 return function inner(){
  console.log(id);
  }
};

const innerFunc = outer();
innerFunc(); // prints 1;
Enter fullscreen mode Exit fullscreen mode

When the outer function returns, its execution context is popped out from the execution stack. But when we invoke the innerFunc() later, it still manages to print out the correct value because LexicalEnvironment of inner function statically captured the “id” binding of its outer (parent) environment when it was created.

// check for binding "id" in the env record of "inner"
-- inner.[[LexicalEnvironment]].[[Record]] --> not found
// if not found, check for its outer environment (outer)
--- outer[[LexicalEnvironment]][[Record]] --> found 1
// resolve the identifier with a value of 1
Enter fullscreen mode Exit fullscreen mode

alt text


Conclusion

  • Execution context stack follows LIFO data structure.
  • There’s one Global context where our code/script is executed.
  • Call to a function creates a new execution context. If it has a nested function call, a new context is created and is put on top of its parent context. When the function finishes executing, it gets popped out of the stack and flow returns back to the context below in the stack.
  • Lexical Environment has two main components: environmentRecord and reference to the outer environment.
  • VariableEnvironment and LexicalEnvironment both statically captured the outer binding for inner functions created in the context. All functions at the creation stage statically(lexically) captures the outer binding of their parent environment. This allows the nested function to access the outer binding even if the parent context is wiped out from the execution stack. This mechanism is the foundation of closures in JavaScript.

I hope this article was fun to read and wasn’t overwhelming. If you liked the article, a few ❤️ will definitely make me smile. Happy coding 😊.

Top comments (5)

Collapse
 
karataev profile image
Eugene Karataev

Nice explanation of internal JavaScript processes.
Also I can recommend this visualizer to see how Execution Contexts are created/modified/removed in runtime.

Collapse
 
aman_singh profile image
Amandeep Singh

Thanks again Eugene. I follow Tyler McGinnis a lot and love the way he explains the complex topic in JavaScript. Have read most of his articles from his blog.

Collapse
 
navidkhm profile image
M.Navid

Thank you @aman_singh;
I was wondering what happens if we initiate function in the Execution Stage like: const x = () =>{our function};
In this way our function just can access to VE in the Execution Stage and not the Creation, so how would the outer VE reference looks llike?

Collapse
 
ms2052001 profile image
Mohammed Samir

Awesome!
Allah bless you!

Collapse
 
aman_singh profile image
Amandeep Singh

Shukran Samir. 🙏