DEV Community

Luigi Cruz
Luigi Cruz

Posted on

JavaScript Execution Context

It is important to view knowledge as sort of a semantic tree — make sure you understand the fundamental principles, ie the trunk and big branches, before you get into the leaves/details or there is nothing for them to hang on to. — Elon Musk on reddit

In this article we will take a look at the most important concept or the fundamental principle of the JavaScript language, that is the Execution Context. The reason for that is because if you have a good understanding of the JavaScript Execution Context, you'll have a much easier time understanding some more advanced topics like hoisting, scopes, scope chains, and closures.

Now with that in mind let's dive right into it.

The Execution Context

Execution context is an abstract concept of an environment where the JavaScript code is executed. You can think of the execution context as a large container that can be used to hold or load things in and process. Within the large container are other smaller containers. Now the question you might ask is, who manages those containers?

The JavaScript Engine

The thing that is responsible for managing the JavaScript execution context is the JavaScript Engine. Each browser has its version of the JavaScript engine. Chrome uses V8 Engine, Firefox uses SpiderMonkey, and Safari uses JavaScriptCore. The first thing the JS engine does is it downloads the JS source code. Once the code is received, it runs into a parser and creates an Abstract Syntax Tree (AST) — a tree-like representation of JS code. After that, it then enters into a global execution context by default. And each invocation or calls to a function on the JS code from this point on will result in the creation of a new local execution context. Every execution context has 2 phases which we will take a look at later in this article. The JS engine also manages the memory allocation (heap memory), garbage collection (Orinoco), code conversion to bytecode (V8 Ignition), and optimization to machine code (V8 TurboFan). The JS engine is another topic of its own so we won't cover that in this article.

Types of Execution Context

  • Global Execution Context (The large container)
  • Local/Function Execution Context (The small container)
  • Eval Function Execution Context (The small container)

Global Execution Context

This is the default execution context that the JS engine enters after it loads and parses the JS code. Once the JS engine is inside the global execution context it will create two properties in the global memory by default, the window object (in the case of browsers) and the this object.

The window object points to or is wired to the global object — an object that is always created before the JS engine enters the global execution context which has properties and methods such as localStorage, innerWidth, event handlers, etc.

The this object (in the global execution context) is an object that points to or is wired to the window object.

So, what will happen if there are variables and functions declared in the JS code? What it will do is it will scan through all the code and look for every variable and function declarations (variables and functions that are not nested to any other function) and store it in the global memory together with window and this objects.

Global Memory
window < ref. to Global Obj. >
this < ref. to window Obj. >
var_identifier var_value
fn_identifier < fn Obj. >

NOTE: The function object is the snapshot of the whole function stored in the memory.

Local/Function Execution Context

Every time a function is called or invoked, a brand new execution context is created for that function. Every function has its local execution context which is created once the JS engine encounters a function call. Inside the local execution context, the JS engine will create an arguments object and a this object by default.

The arguments object contains a key:value pair of parameters expected inside a function. It also contains a default property called length, that counts the numbers of parameters for that function. The arguments object defaults to { length: 0 } when the function's argument is empty.

The this object inside the function execution context varies depending on how the function is called. If it is called by an object reference, then the value of this is set to that object. Otherwise, the value of this is set to the window object or will be of value "undefined" (in strict mode).

Local Memory
arguments { length: 0 }
this < ref. to window Obj. >
var_identifier var_value
fn_identifier < fn Obj. >

Eval Function Execution Context

The eval function is a dangerous function. Whenever the JS engine encounters an eval() function, an execution context is created and is pushed into the call stack. It accepts a string as a parameter and evaluates it. So if you accidentally passed a malicious code in its argument, or a malicious party exploits this part of your code, then your website could potentially be severely damaged. It is not recommended to use this function as there are better alternatives to it. You can learn more about eval function here.

How does the JS engine know the currently running Execution Context?

JavaScript is a single-threaded language which means that it can only run a single task at a time. An example of a task is declaring a variable, assigning a value to a variable, or calling a function. We already know that a function call sets up a local/function execution context. Under the hood when the JS engine encounters a function call its task is to push that execution context into memory and pops it off when the code inside of it is done. That memory is called the Execution Stack also known as the "Call Stack". It is an array of execution contexts that uses a LIFO (Last In First Out) data structure. It is used by the JS engine to keep track of execution context by storing each call into the memory. The global execution context is present by default in the call stack and it is at the bottom of the stack. While executing the global execution context code, if the JS engine finds a function call, it creates a local execution context for that function and pushes it to the top of the call stack. The JS engine then executes the function whose execution context is at the top of the call stack. Once all the code of the function is executed, JS engine takes out that function execution context and start’s executing the function which is below it.

Let us try to understand that with an example:

console.log("Initially I am inside the global execution context.");

let message = "Heyyow!";

function first() {
  console.log("I am inside the first function execution context");
  second();
  console.log("I am again inside the first function execution context");
}

function second() {
  console.log("I am inside the second function execution context");
}

first();
console.log("I am back at the global execution context.");
Enter fullscreen mode Exit fullscreen mode

Call stack process

View a PDF copy of the Call stack process here.

Once the JS engine is done loading and parsing the JS code, it will set up a global() execution context and pushes it to the call stack. When it sees the function call first(), it will set up a new function execution context for that function and pushes it to the top of the call stack. When the JS engine encounters the function call second() inside the first execution context, it will create another execution context for that function and pushes it to the top of the call stack. Once the second() function finishes, its execution context is popped off from the call stack, and the control is transferred to its parent execution context first(). When the first execution context is finished, its execution context is removed from the call stack, and the control is transferred to its parent execution context global(). Once all the code is executed, the JS engine removes the global execution context from the call stack and exits.

What exactly happens inside an Execution Context?

Now that we already know that the JS engine uses the call stack to keep track of the currently running execution context, let us now understand what exactly is happening inside an execution context.

The execution context has two phases: 1) the Creation Phase and 2) the Execution Phase.

Creation Phase

This is the stage that the JS engine enters after the JS code is loaded and has been parsed. Every execution context has a Creation Phase. The creation of an execution context is part of the creation phase. Two state components are created during the creation phase:

  • Lexical Environment, and
  • Variable Environment

Conceptually, the execution context is represented as follows:

GlobalExecutionContext = {
  LexicalEnvironment : { },
  VariableEnvironment : { },
}
Enter fullscreen mode Exit fullscreen mode

A Lexical Environment component is a structure that defines the association of identifiers to the values of variables and functions based upon the lexical nesting structure of JS code. This association of identifier to the values of variables and functions is called binding.

A Variable Environment component is also a Lexical Environment that defines the association of identifiers to the values of variables but not functions.

The difference between the two is in the variable that the identifier is bounded. The Lexical Environment is used to store bindings of an identifier to the values of the variables (let and const) and functions, while the Variable Environment is used to store bindings of an identifier to the values of the variable (var) only.

I'm confused. What is exactly inside the Lexical Environment?

Environment Record

Each Lexical Environment has an Environment Record. The Environment Record records the identifier bindings that are created within the scope of the lexical environment. Each time a JS code is evaluated (var/func declarations or assignments), a new Environment Record is created to record the identifier bindings that are created by that code. This environment in JavaScript is called the scope.

Every Environment Record has an [[OuterEnv]] field, which is either null or a reference to an outer Environment Record. The reason why a child function has access to its parent's scope is because of the outer environment object. For example, when the JS engine sees a variable inside a function, it will try to find the variable's value from the current function's environment record (local memory). If it could not find the variable inside of it, it will look into the outer scope (its parent environment record) up to the global scope until it finds that variable. This lookup process is called the scope chain. We'll try to take a look at the scope chain and dig deeper in future articles.

There are three type subclasses inside of the Environment Record:

  • Declarative Environment Record — As its name suggests stores variables, classes, modules, and/or function declarations. A declarative Environment Record binds the set of identifiers defined by the declarations contained within its scope.
  • Object Environment Record — This environment record in the global execution context contains the bindings for all built-in globals. This is the window object that references the global object. Variables and functions that are of global scope are added to the global execution context's object environment record that is why you can access global variables such as window.localStorage and window.var_name. In the local/function execution context the object environment record is composed of the arguments object and the this object.
  • Global Environment Record — A global Environment Record is logically a single record but it is specified as a composite encapsulating an object Environment Record and a declarative Environment Record. It does not have an outer environment; it's [[OuterEnv]] is null. It may be prepopulated with identifier bindings and it includes an associated global object whose properties provide some of the global environment's identifier bindings. As JS code is executed, additional properties may be added to the global object and the initial properties may be modified.

Now, let us conceptually visualize the execution context inside the Creation Phase. Take a look at this JS code:

var name = "Luigi";
let input = "Hello, World!";

function broadcast(message) {
  return `${name} says ${message}`;
}

console.log(broadcast(input));
Enter fullscreen mode Exit fullscreen mode

Note that when I say conceptually, it means that the pseudocode below is not the concrete representation of the environment the JS engine creates, but only to learn the concept by trying to visualize it.

GlobalExecutionContext = {
  LexicalEnvironment : {
    EnvironmentRecord : {
      DeclarativeEnvironmentRecord : {
        input: <uninitialized>,
        broadcast: < function broadcast(message) {
          return `${name} says ${message}`;
        } >,
      }, // Bindings of identifier to variables (`let` and `const`) and identifier to function objects
      ObjectEnvironmentRecord : {
        window: < ref. to Global obj. >,
        this: < ref. to window obj. >,
      },
      OuterEnv : < null >, // ref. to parent env. record (null in here since global has no parent execution context)
    }, 
  },
  VariableEnvironment : {
    EnvironmentRecord : {
      DeclarativeEnvironmentRecord : {
        name: undefined,
      }, // Bindings of identifier to variables (`var`)
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Let's try to go through each step of what's happening inside the Creation Phase using the code snippet above:

  1. The JS engine enters the Creation Phase.
  2. Creates a global execution context and pushes it into the call stack.
  3. Create bindings for the window object to the Global object.
  4. Create bindings for the this object to the window object. Note that this object binding will vary depending on how the function is called and on strict mode.
  5. Creates an identifier name in the global memory and initializes it with a value of undefined. This process is called hoisting.
  6. Creates an identifier input in the global memory without initializing it or no initial value set.
  7. Creates an identifier broadcast in the global memory and store the whole function definition of the broadcast function in it. This function is also hoisted.

Next, we'll talk about how does the JS code gets executed.

Execution Phase

This is the stage that the JS engine enters after all variables and functions are declared and necessary objects have bounded. Every execution context has an Execution Phase. Few things are happening inside this phase, the variable binding initializations, variable assignments, mutability and immutability checking, variable binding deletions, function call execution, etc.

Let's try to understand that by continuing the steps we wrote from the Creation Phase:

  1. The JS engine enters the Creation Phase.
  2. Creates a global execution context and pushes it into the call stack.
  3. Create bindings for the window object to the Global object.
  4. Create bindings for the this object to the window object. Note that this object binding will vary depending on how the function is called and on strict mode.
  5. Creates an identifier name in the global memory and initializes it with a value of undefined. This process is called hoisting.
  6. Creates an identifier input in the global memory without initializing it or no initial value set.
  7. Creates an identifier broadcast in the global memory and store the whole function definition of the broadcast function in it. This function is also hoisted.
  8. The JS engine enters the Execution Phase.
  9. Take the value of variable name and bind that value to the identifier inside the memory.
  10. Take the value of variable input and bind that value to the identifier inside the memory.
  11. Encounters a console log method, immediately evaluate the arguments inside it.
  12. Sees a function call named broadcast, immediately creates a new local execution context for that function, and pushes it into the top of the call stack.
  13. Enters the Creation Phase of the broadcast function execution context.
  14. Creates arguments object, in the function's local memory with an initial value of { length: 0 }.
  15. Add the passed parameter message to the first index of the arguments object.
  16. Creates an identifier message in the function's local memory and store the value that is passed to the function call's argument.
  17. Goes inside the function block and evaluates the return statement.
  18. Sees a variable name, it then performs a lookup of that variable inside the function's local memory.
  19. It couldn't find the identifier name in the local memory so it then continues to look for it from its parent scope (global memory).
  20. It finds the identifier name in the global memory so it takes that value and swap it to the variable reference.
  21. Sees a variable message, it then performs a lookup of that variable inside the function's local memory.
  22. It finds the identifier message in the local memory so it takes that value and swaps it to the variable reference.
  23. Returns the evaluated result of the broadcast function execution context and is popped off from the call stack.
  24. Pass the control to its calling context (the global execution context) with the returned result.
  25. Displays Luigi says Hello, World! in the console.
  26. Global execution context is popped off from the call stack and then the JS engine exits.

Whew, that's a lot! There's a lot more that is happening inside the Execution Phase like object mutability and immutability checking, etc... but I tried to simplify the steps so that I won't complicate the main idea in this article. But if you wanted to dig more though, you can read through at those from the JavaScript Spec.

So with all of that here's an updated pseudocode for all the execution context:

GlobalExecutionContext = {
  LexicalEnvironment : {
    EnvironmentRecord : {
      DeclarativeEnvironmentRecord: {
        input: "Hello, World!",
        broadcast: {
          LocalExecutionContext : {
            LexicalEnvironment : {
              EnvironmentRecord: {
                DeclarativeEnvironmentRecord: {
                  message: "Hello, World!",
                },
                ObjectEnvironmentRecord: {
                  arguments: { 0: message, length: 1 }
                  this: < ref. to window obj. >
                },
              },
              OuterEnv: < ref. to LexicalEnvironment of the GlobalExecutionContext >,
            },
          },
        },
      },
      ObjectEnvironmentRecord: {
        window: < ref. to Global obj. >,
        this: < ref. to window Obj. >,
      },
      OuterEnv: < null >,
    },
  },
  VariableEnvironment : {
    EnvironmentRecord : {
      DeclarativeEnvironmentRecord: {
        name: "Luigi"
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

To further explain the whole concept, I've created a high-level representation of the step-by-step process of the Execution Context using a GIF below.

Step by step execution process of JavaScript source code

View a PDF copy of the Execution context process here.

There are few interesting topics that we didn't try to dive into deeper in this article. These are the this object, scope chain, hoisting, garbage collection, etc. We'll try to discuss those in future articles.

I know that's a lot of things to absorb. You'll probably have to revisit this post multiple times to understand. While you don't need to learn all these concepts to be a good JavaScript developer, having a decent understanding of the above concepts will help you clear out those fogs at the more advanced topics. As Elon Musk said, focus on understanding the fundamental principles of things first, and later on you'll be surprised by the topic that you thought doesn't make sense — actually, isn't so hard at all.

Anyways, that's all I could share. I'll see you in my next post!

Oldest comments (5)

Collapse
 
javier123454321 profile image
Javier Gonzalez

Now this is quality content! Thank you. I especially liked thinking about closures in terms of having access to an object called [[OuterEnv]]. It makes sense to talk about it this way. A lot to unpack here.

Collapse
 
luigircruz profile image
Luigi Cruz

Thank you so much for the kind words Javier. Yeah, you're right. Closures will really explain well the usage of the [[OuterEnv]] but I didn't try to introduce it as it is more of an advance topic.

Collapse
 
sudarshansb143 profile image
sudarshan

Nice Article !

it feels like Ryan Dalh explaining it

:)

Collapse
 
luigircruz profile image
Luigi Cruz

Wow! You're so kind Sudarshan. Thank you so much!

I really admire Ryan Dahl's works so I feel so humbled being compared to him. This made my day! :)

Collapse
 
ibrahim-elhofy profile image
Ibrahim Elhofy • Edited

The article is great and includes a lot of concepts that you have successfully explained
but I think there are two mistakes

  1. In Execution Phase we don't put the Execution Context of function in his outer Execution Context, we push it into Execution Stack
. . . . .
     broadcast: {
          LocalExecutionContext : {
            LexicalEnvironment : {
              EnvironmentRecord: {
                DeclarativeEnvironmentRecord: {
                  message: "Hello, World!",
                },
                ObjectEnvironmentRecord: {
                  arguments: { 0: message, length: 1 }
                  this: < ref. to window obj. >
                },
              },
              OuterEnv: < ref. to LexicalEnvironment of the GlobalExecutionContext >,
            },
          },
        },
. . . .
Enter fullscreen mode Exit fullscreen mode
  1. In ObjectEnvironmentRecord of GlobalExecutionContext you put window and this objects without their properties despite they are binding with it

if i understanding it wrong i will be appreciated if you explain to me