How JavaScript Executes Your Code: The Concept of Execution Context Explained

How the JavaScript Engine Creates and Manages Execution Contexts for Your Code

ยท

11 min read

How JavaScript Executes Your Code: The Concept of
Execution Context Explained

JavaScript is a powerful and popular programming language that can make web pages come alive with dynamic and interactive features ๐Ÿš€. But have you ever wondered how JavaScript runs your code? How does it know what variables and functions you have defined and where to find them? How does it handle the order and flow of your code and deal with different situations? To answer these questions, you need to learn about a key concept in JavaScript called execution context ๐Ÿง . In this article, we will explore how JavaScript executes your code using the concept of execution context, how the global and function execution contexts are created and how they work ๐Ÿ› ๏ธ, how the call stack and scope chain are used to find and access variables and functions in different scopes ๐Ÿ”Ž. We will also show you how this keyword works in different execution contexts ๐Ÿ‘‰.

How JavaScript Code Gets Executed: ๐Ÿค”

You might not know this, but the browser cannot directly run the JavaScript code that we write in our applications. It needs to change into a format that the browser and our computers can understand โ€“ machine code ๐Ÿ’ป.

When the browser reads the HTML file, and finds some JavaScript code to run inside a <script> tag or an attribute that has JavaScript code like onClick, it sends it to a special part of the browser called the JavaScript engine ๐Ÿš—.

The JavaScript engine is responsible for transforming and running the JavaScript code. To do this, it creates a special environment where it can store and manage the code. This environment is called the Execution Context ๐ŸŒŽ.

The Execution Context is a special environment where JavaScript code is executed. It contains the code that is currently running, and everything that helps it to run ๐Ÿƒโ€โ™‚๏ธ.

When the JavaScript engine executes a piece of code, it goes through two phases: the creation phase and the execution phase. In the creation phase, the engine does the following: ๐Ÿ› ๏ธ

  • It creates a variable object (VO) or a lexical environment (LE) to store the variables and functions that are declared in the code ๐Ÿ“ฆ.

  • It creates a scope chain to keep track of the scopes that are accessible to the code ๐Ÿ”—.

  • It determines the value of this keyword, which depends on how the code is invoked ๐ŸŽฏ.

In the execution phase, the engine does the following: ๐Ÿš€

  • It parses the code line by line and assigns values to the variables and functions in the VO or LE ๐Ÿ“.

  • It looks up the scope chain to resolve any identifiers that are not found in the current scope ๐Ÿ”.

  • It executes the code and returns a value ๐Ÿ’ฏ.

There are two types of Execution Context in JavaScript: Global and Function. Letโ€™s take a detailed look at both ๐Ÿง.

Global Execution Context (GEC): ๐ŸŒŽ

The Global Execution Context (GEC) is the first and main execution context that is created when your script file is loaded by the browser. It is where all the code that is not inside a function is executed. There is only one GEC for each script file because the JavaScript engine is single-threaded and can only run one execution context at a time ๐Ÿ•’.

Function Execution Context (FEC) ๐Ÿš€

Whenever a function is called, the JavaScript engine creates a different type of Execution Context known as a Function Execution Context (FEC) within the GEC to evaluate and execute the code within that function.

Since every function call gets its own FEC, there can be more than one FEC in the run-time of a script.

The creation of an Execution Context (GEC or FEC) happens in two phases: ๐Ÿ› ๏ธ

  1. Creation Phase

  2. Execution Phase

Creation Phase: ๐ŸŽจ

The creation phase occurs in 3 stages, these stages are:

  1. Creation of the Variable Object (VO) ๐Ÿ“ฆ

  2. Creation of the Scope Chain ๐Ÿ”—

  3. Setting the value of the this keyword ๐ŸŽฏ

Creation Phase: Creation Of The Variable Object (VO) ๐Ÿ“ฆ

The Variable Object (VO) is an object-like container created within an Execution Context.

The VO stores the variables and functions that are declared in the code so that they can be accessed and used later ๐Ÿ˜Š.

function sayHello(name) {
  var greeting = "Hello, " + name;
  console.log(greeting);
}

The VO of the Execution Context for this function will have two properties: name and greeting. The name property will hold the value of the parameter that is passed to the function and the greeting property will hold the value of the variable that is created inside the function. The VO also has a reference to the Global Object, which is the window object in browsers. This means that you can access global variables and functions from inside the function as well.

VO of this code will look like this in the creation phase :

VO = {
  name: undefined,
  greeting: undefined,
  sayHello: function sayHello(name) {
    var greeting = "Hello, " + name;
    console.log(greeting);
  }
}

This process of storing variables and function declaration in memory before the execution of the code is known as Hoisting.

Letโ€™s talk more about this.

Hoisting in JS: ๐Ÿšฉ

Function and variable declarations are hoisted in JavaScript. This means that they are stored in the memory of the current Execution Contextโ€™s VO and made available within the Execution Context even before the execution of the code begins ๐Ÿ. This means that you can use a variable or a function before it is defined in the code, as long as it is declared somewhere in the same scope ๐Ÿ™Œ.

I will explain more about Hoisting in the next blog ๐Ÿ“.

Creation Phase: Creation of The Scope Chain ๐Ÿ”—

After the creation of the Variable Object (VO) comes the creation of the Scope Chain as the next stage in the creation phase of an Execution Context.

Each function execution context creates its scope. Scope means how accessible a part of the code is to another part of the code ๐Ÿ™Œ. Look at the below example:

A scope is a section of code that has its own set of variables and functions. In this code, there are three scopes: the global scope, the foo function scope, and the bar function scope.

The global scope contains the global variable x and the function declarations of foo and bar. The foo function scope contains the local variable y and a reference to the global scope as its parent scope. The bar function scope contains the local variable z and a reference to the foo function scope as its parent scope.

The bar scope is the innermost scope, which can access its variable z, as well as the variables x and y from its parent scopes. The foo scope is the parent scope of the bar, which can access its variable y, as well as the global variable x. The global scope is the outermost scope, which can only access its variable x.

When JavaScript needs to look up a variable or a function, it starts from the current scope and searches for it in the Variable Object (VO), which is the part of the Execution Context that stores the variables and functions for that scope. If it does not find it in the current scope, it moves up to the parent scope and searches for it in its VO. It repeats this process until it reaches the global scope and searches for it in its VO. If it still does not find it, it throws a ReferenceError.

For example, when JavaScript executes the console.log(x + y) statement inside the foo function, it needs to resolve the values of x and y. It first looks for them in the current scope, which is the VO of foo. It finds y in the VO, but it does not find x. It then looks up in the scope chain and searches for x in the parent scope, which is the global VO. It finds x in the global VO and uses its value. It then adds x and y and prints the result to the console.

When JavaScript executes the console.log(y + z) statement inside the bar function, it needs to resolve the values of y and z. It first looks for them in the current scope, which is the VO of the bar. It finds z in the VO, but it does not find y. It then looks up in the scope chain and searches for y in the parent scope, which is the VO of foo. It does find y in that VO and uses its value. It then adds y and z and prints the result to the console.

This idea of the JavaScript engine traversing up the scopes of the execution contexts that a function is defined in to resolve variables and functions invoked in them is called the scope chain. The scope chain works as a one-way glass. You can see the outside, but people from the outside cannot see you.

Creation Phase: Setting The Value of The โ€œthisโ€ Keyword ๐ŸŽฏ

The next and final stage after scoping in the creation phase of an Execution Context is setting the value of the this keyword ๐Ÿ˜Š. In JS this keyword refers to the scope where an Execution Context belongs ๐Ÿ™Œ. The value of this depends on how the function is invoked, and it can be different for each Execution Context ๐Ÿค”. For example, in a global function, this refers to the global object (window in browsers), but in an object method, this refers to the object itself ๐Ÿš—. You can also use methods like call(), apply(), and bind() to explicitly set the value of this for a function. We will study more about this keyword next blog as of now let's move to the execution phase ๐Ÿš€.

The Execution Phase

Finally, right after the creation phase of an Execution Context comes the execution phase. This is the stage where the actual code execution begins.

Up until this point, the VO contained variables with the values of undefined. If the code is run at this point it is bound to return errors, as we can't work with undefined values.

At this stage, the JavaScript engine reads the code in the current Execution Context once more and then updates the VO with the actual values of these variables. Then the code is parsed by a parser, transpired to executable byte code, and finally gets executed.

Initially, the global execution context is created and pushed to the call stack. It contains the global variable x and the global functions foo.

Then, the code starts executing from top to bottom. The variable x is assigned the value 10. The function foo is defined and stored in memory. The last line of the code calls the function foo.

When the function foo is called, a new execution context for foo is created and pushed to the top of the call stack. It contains the local variable y, the function bar and a reference to the outer environment (the global context).

The execution of foo starts and assigns the value 20 to the variable y. Then, it prints the value of x + y, which is 30. It looks for x in its context but does not find it. Then, it looks up in the scope chain and finds x in the global context.

Next, it calls the function bar.

When the function bar is called, a new execution context for the bar is created and pushed to the top of the call stack. It contains the local variable z and a reference to the outer environment (the foo context).

The execution of the bar starts and assigns the value 30 to the variable z. Then, it prints the value of y + z, which is 50. It looks for y in its context but does not find it. Then, it looks up in the scope chain and finds y in the foo context.

After that, there are no more lines of code to execute in the bar, so it returns and its execution context is popped from the call stack.

Similarly, there are no more lines of code to execute in foo, so it returns and its execution context is popped from the call stack. Finally, there are no more lines of code to execute in the global context, so the script ends and the global execution context is popped from the call stack.

Conclusion:

Let's now try to write the execution context for the above code:

For this code, there are three Execution Contexts: one for the global scope, one for the foo function scope, and one for the bar function scope. The global Execution Context is created when the script is executed, and it remains until the end of the script. The foo Execution Context is created when the foo function is invoked, and it remains until the foo function returns. The bar Execution Context is created when the bar function is invoked, and it remains until the bar function returns.

The global Execution Context will look like this in the creation phase:

globalEC = {
  VO: {
    x: undefined,
    foo: function foo() {
      var y = 20; // local variable of foo
      console.log(x + y); // 30
      bar();
            bar: function bar() {
          var z = 30; // local variable of bar
          console.log(y + z); // 50
    }
    },
  },
  Scope Chain: [globalVO],
  this: window
}

The global Execution Context will be updated like this in the execution phase:

globalEC = {
  VO: {
    x: 10,
    foo: function foo() {
      var y = 20; // local variable of foo
      console.log(x + y); // 30
      bar();
            bar: function bar() {
          var z = 30; // local variable of bar
          console.log(y + z); // 50
    }
    },
  },
  Scope Chain: [globalVO],
  this: window
}

The foo Execution Context will look like this in the creation phase:

fooEC = {
  VO: {
    y: undefined,
    bar: function bar() {
           var z = 30; // local variable of bar
           console.log(y + z); // 50
         },
    arguments: [],
    parentScope: globalVO
  },
  Scope Chain: [fooVO, globalVO],
  this: window
}

The foo Execution Context will be updated like this in the execution phase:

fooEC = {
  VO: {
    y: 20,
    bar: function bar() {
            var z = 30; // local variable of bar
            console.log(y + z); // 50
           },
    arguments: [],
    parentScope: globalVO
  },
  Scope Chain: [fooVO, globalVO],
  this: window
}

The bar Execution Context will look like this in the creation phase:

barEC = {
  VO: {
    z: undefined,
    parentScope: fooVO
  },
  Scope Chain: [barVO, fooVO, globalVO],
  this: window
}

The bar Execution Context will be updated like this in the execution phase:

barEC = {
  VO: {
    z: 30,
    parentScope: fooVO
  },
  Scope Chain: [barVO, fooVO, globalVO],
  this: window
}
ย