avatarMoon

Summary

This context explains advanced JavaScript concepts such as execution context, lexical environment, and closures.

Abstract

The provided context delves into advanced JavaScript concepts, starting with the explanation of execution context. It explains that whenever a function is invoked, a new execution context is created and stored in the execution context stack. The context also discusses lexical environment, which keeps track of variables, function names, and associated values within an execution context. Furthermore, the concept of closures is explained, which is a function that has access to its parent function's scope even after the parent function has finished execution.

Opinions

  1. The context emphasizes the importance of understanding execution context, lexical environment, and closures for advanced JavaScript development.
  2. It explains that the creation of new execution contexts can lead to a stack overflow error if there are too many contexts in the stack.
  3. The context highlights that variables are declared and set to their default value during the creation phase, while functions are declared and initialized at once.
  4. The context explains that even after an execution context is deleted from the stack, a reference to the function is alive when the original function returns a new function that uses a variable out of its scope.
  5. The context explains that closures allow access to the parent function's scope, even after its execution phase is finished.
  6. The context provides resources for further learning on the topics discussed, including links to the ECMAScript specification and a JavaScript visualizer.
  7. The context concludes by summarizing the key concepts discussed, emphasizing the importance of understanding execution context, lexical environment, and closures for advanced JavaScript development.

Execution Context, Lexical Environment, and Closures in JavaScript

Advanced JavaScript concepts you should know

Photo by Javier Sierra on Unsplash

In this piece I want to talk about three advanced JavaScript concepts: execution context, lexical environment, and closure.

This will be a long post. If you want to skip to the summary, scroll to the bottom of the page.

What is an Execution Context?

So, what is an execution context? Whenever you write some code, your code is in a space — that space is called the “execution context”. Imagine you wrote a simple calculator:

function cal(type, a, b) {
  if (type === 'add') {
    return a + b;
  } else if (type === 'subtract') {
    return a - b; 
  } else if (type === 'multiply') {
    return a * b; 
  } else {
    return a / b; 
  }
}

var four = 4;
var seven = 7;
cal('add', 4, 7);

In JavaScript, whenever a function is invoked, a new execution context is created on the execution context currently running. A newly created one is stored in the stack for execution contexts.

So, if we call cal(), the new context will be created and it will be pushed to the context stack. But, by default, there’s an already existing context in the stack — a global execution context.

The basic concept of an execution context

First the function cal is called and run so the new execution context is created. Then it is stored in the context stack. Then the control of the current execution context is transferred to the newly created one from, in this case, the global context.

An execution context is created whenever you call a function. When developing in JavaScript, you might have seen this error:

Call stack error

The function a keeps calling itself recursively. Every time a calls itself, a new execution context about a will be created and stored in the stack. Since the memory stack’s storage space isn’t infinite, it overflows.

Now you know what an execution context is, but there is more to learn. We’ll talk about it further shortly.

A Lexical Environment

What happens when you run a function or when you declare a variable or a function? I barely explained that an execution context is created on every function calls and there’s a global execution context in the stack by default. An execution context is divided into three different areas.

Inside an execution context

The first two sections in an execution context — LexicalEnvironment and VariableEnvironment, are very similar, so I think I can call them just LexicalEnvironment. What LexicalEnvironment does is to keep track of variables, function names and associated values. In other words, if you declare a function foo like a figure above, then the LexicalEnvironment would look like this:

function foo() {
  var a = 10;
  function bar() {
    
  }
}
foo();

// When foo is called, a new execution environment 
// might look like this below
execution_environment: {
  LexicalEnvironment: {
    a: 10,
    bar: function() {}
  },
  ThisBinding: ...
}

You now know what LexicalEnvironment is and what it does.

Just as an execution context consists of three parts, LexicalEnvironment also consists of a few parts. It has anEnvironmentRecord — I will call it an environment record, and an outer lexical environment that you may think it’s scope.

When you declare a variable or a function, they are actually stored in its environment record. Chaining the value of property basically means chaining the value of a property that belongs to an environment record.

A reference to the outer lexical environment is also created when LexicalEnvironment is created. It directly links to the parent LexicalEnvironment, and JavaScript uses this value when it can’t find a property in the current LexicalEnvironment. If it still can’t find it in the parent LexicalEnvironment, then it, again, goes up to the parent LexicalEnvironment. This process doesn’t stop until it finds what it’s looking for or until nothing is connected to the LexicalEnvironment. The global LexicalEnvironment doesn’t have a parent's LexicalEnvironment. So, when JavaScript tries to look for something in the global LexicalEnvironment, a Reference Error occurs.

Example with figures

To understand this flow better, let’s take a look at this example:

var x = 1;
function foo() {
  var y = 2;
  function bar() {
    var z = 3;
    function baz() {
      console.log(z);
      console.log(y);
      console.log(x);
      console.log(w);
    }
    baz();
  }
  bar();
}
foo();
// 3
// 2
// 1
// Reference Error: w is not defined
The global LexicalEnvironment

When JavaScript runs the code, the variable x and the function foo are declared in the global LexicalEnvironment. As I explained, the global’s outer doesn’t point to anything. When JavaScript meets foo() , it runs the function and the new LexicalEnvironment of foo is created. Its outer link links to its parent’s globalLexicalEnvironment. This figure illustrates the whole relationship in the example code:

The entire relationship amongst LexicalEnvironments

You can see outer in each LexicalEnvironment points to the parent’s LexicalEnvironment.

When baz() is called, it looks for z, y , x , and w.

The workflow when z and w are looked for

Like this flow, finding the variable y and x does the same work — checking whether or not to exist the value in the current LexicalEnvironment and moving to the parent’s one if there’s no variable. But for w , it doesn’t exist anywhere in the codes so we end up with a reference error.

This is what you normally heard, a scope chain. But remember, the outer environment, connecting to the parent’s environment, is determined when the function is declared, not when it’s invoked (Click here to get further information). For instance, guess the return value of bar().

var x = 1;
function foo() {
  var x = 2;
  bar();
}

function bar() {
  console.log(x); 
}

foo(); // 1

Why is the result of foo() 1 and not 2? As I said, the outer reference is the parent’s LexicalEnvironment, not the function surrounding it.

ThisBinding

The reference inside the execution context, ThisBinding, determines how the function is called. I’ll talk about this topic more in another piece.

Back to Execution Context

Now, you know what are in an execution context — LexicalEnvironment and ThisBinding. There are actually two kinds of execution context — global execution context and function execution context.

The global execution context is the execution context for the global object in JavaScript, which is the root of everything. What it contains are several objects. One of them is window — when JavaScript parses and interprets your script, it actually runs this first. (Of course, this is not exactly how JavaScript works).

<script>
  window = {
    ClipboardCopyElement: class ClipboardCopyElement,
    CodeMirror: f Ea,
    DetailDialogElement: class DetailDialogElement,
    ...
  };
  
  GlobalExecutionContext: {
    LexicalEnvironment: {
      window: window,
      outer: null
    },
    ThisBinding: window
  }
</script>

That’s why you could use window.document or window.setTimeout, since they are all in the LexicalEnvironment of the global object.

What about a function execution context? It’s just like a global execution context but no window or other global objects exist inside and it is created when a function is called. What the global execution context and function execution contexts have in common is that they both have two phases at runtime: creation and execution phases. In the creation phase, variables and functions are declared:

var x = 1;
function foo() {
  var x = 2; 
  function bar() {
    var x = 3; 
  }
}
console.log(y);
var y = 3;

foo();

The first step on running the code is that the global execution context is created. The control of action is in the creation phase.

The creation phase — image source is from here

In the creation phase, variables are set by undefined by default. functions, on the other hand, are assigned by a function — like foo in the example above. The thing to note is that y is also defined as undefined as well, even though it’s declared after console.log . This symptom is called “hoisting” in JavaScript. Hoisting means variables and functions are declared and assigned with the default value, normally undefined , before the execution phase.

When the control moves to the execution phase, it runs every piece of code from the top. The first statement that will be executed is console.log(y) and it prints undefined out. Then y is set to three. The next step is to run foo(). When a function is called, a new execution context is created and pushed into the call stack.

The creation phase for a function — image source is from here

When a function is called, a function execution context is made. Then its creation phase starts first. The LexicalEnvironment of the function execution context defines all of the variables it needs. The special object, arguments, is a part of variables that are defined in every function LexicalEnvironment. You can see there’s a function bar(). Then the bar is invoked — its execution context will also be stored in the stack after being created.

Once all of the codes in a function are executed, its execution context is removed from the stack.

In this video, the speaker does a good job explaining the workflow of execution context switching in JavaScript.

Closures

I explained that LexicalEnvironment also has an outer environment that refers to the parent’s LexicalEnvironment. Take a look at this example:

var x = 1;
function foo(y) {
  return function(z) {
    return x + y + z;	
  }
}

var f = foo(2);

If you run this command on the web console, you’ll see this:

console.dir(f);
Closure looks like this

On the left is what you can see from the google console and on the right is what you can see from the JavaScript Visualizer — the web page you can see here.

So, what happened? When the function foo was executed, it returned a new function. f now has the return value of foo . But in f , there’s a new scope object, named Closure. Even though the execution context of foo was removed from the stack, the referencing chains are still alive by the variable f . Then what if we execute f , now which is a function?

f(5);
An execution context in the closure — image source is from here

Then the new execution context will be created like the picture above. The reason why z isn’t in the closure scope, is because it’s a property of the function that f ran, which is alive in the current execution context.

Once return x + y + z; is run and the function is finished, the execution context for f will be gotten rid of, and the closure will also be gone.

MDN defines closure:

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function.

Closure is a function that allows you to access to the parent function scope, even though it’s been removed from the execution context stack.

Final Summary

That was a long story to explain what execution context, LexicalEnvironment, closure, etc. Basically, when JavaScript runs codes, it creates a space for storing and managing variables and functions. It keeps track of the names and changes. Whenever a function is created, JavaScript makes this spaceand puts it on the top of the stack, where previously existing those spaces are stacked from the bottom. This space is called the execution context.

There are two execution contexts in JavaScript — the global execution context and the function execution context. An execution context is consist of LexicalEnvironment and ThisBinding. LexicalEnvironment is the place where variables and functions are actually stored and managed. ThisBinding is the reference for this but we didn’t cover that here.

JavaScript has two phases when it comes to managing variables and functions — creation phase and execution phase. During the creation phase, variables are declared but set by the default value, normally undefined, while functions are declared and initialized at once. After the creation phase, the action control moves to the execution phase, where all of the codes are executed, one by one, from the top of the file. If a function is invoked during this phase, a new execution context for that function will be created and stored in the stack. If there are too many contexts in the stack, we also confirmed that reference errors occur.

Even though an execution context is deleted from the stack once it has run all the codes inside, a reference to the function is alive when the original function returns a new function that uses a variable out of its scope. Then, the link to the outer function won’t be deleted until its codes are executed and completely removed from the stack. This is called Closure. Closure is a function that allows you to access the parent’s function scope, even though its execution phase is finished.

Resources

JavaScript
Closure
Programming
Coding
Web Development
Recommended from ReadMedium