Execution Context and Scope Chain in JavaScript

Execution Context and Scope Chain in JavaScript

In this post, we will discuss what is execution context and scope chain in JavaScript. This is a very interesting and essential topic for those who want to know how JavaScript works under the hood. This post gives you a deeper understanding of how a JavaScript code gets executed in a web browser, and the processes that take place behind the scenes.

Execution Context

Whenever a javascript code is executed in a browser, the JavaScript engine creates a special environment to handle the execution of this JavaScript code. This environment is known as the Execution Context.

Execution context is of two types

  1. Global execution context (GEC): The global execution context is created when a JavaScript script first starts to run. In this phase, it creates the global window object(when run in browsers) and also allocates memory for the variables and functions that are in the global scope(not inside any function). This represents the global scope of JavaScript.

  2. Function execution context (FEC): Anytime you execute or invoke a function in JavaScript a new execution context is created. This execution context will have its own space for variables and functions. This represents the function's local scope.

Execution context is created in two phases

  • Creation phase

  • Execution phase

Creation Phase

Creation phase includes creation of the following objects

  • Creation of global object: if it is global execution context, the window object is created in the browser and global object is created in NodeJs.

  • Setting up the value of this keyword: This keyword is the same as the window object at the global level in the case of a browser.

  • Setting up scope chain: It is a link to outer environment. In case of GEC there is no further outer environment, so it will be null.

  • The parser runs through the code and sets up memory space for variables and functions.

While the parser runs through the code line by line, it creates a memory space for each of the variables and functions it encounters. Also, the references to these variables are stored in this keyword.

For any variable which is declared using var keyword, the default initial value is set as undefined. JavaScript doesn't throw an error when a variable is used before it is defined since the execution context is already aware of its existence in the code.

When it comes to functions, the entire function is stored in the memory, and since we have the reference for this function in our execution context the function calls we do even before declaring the function will also run perfectly fine. This process of allocating memory space for variables and functions before execution of code is called "Hoisting".

Because of hoisting, the variable a and function b in the example below are available prior to their declaration.

console.log(a);  // undefined
b();    // function b called!

var a = "Hello world!";
function b(){
    console.log("function b called!");
}

However, the assignments are set in the execution phase when the code is executed line by line. Hence the value of a is undefined when it is used before its declaration.

There is a special case when it comes to function assignments. In the example below we anticipate the code to work fine as getDiscount is a function.

getDiscount(100);
var getDiscount = function (price) {
    console.log(price * 0.1); 
};

But when we run the code it throws "TypeError: getDiscount is not a function". This is because getDiscount is a variable which is declared using var keyword. So getDiscount will be hoisted as a variable and not as a function, which means getDiscount will have a default initial value of undefined until the assignment statement in line 2 is executed. Hence it gives us the TypeError.

Execution Phase

After the creation phase of the Execution Context comes the execution phase. This is the stage where the actual code execution begins. In this phase, the Javascript Engine runs our code line by line. The code is parsed by a parser, compiled to executable byte code, and finally gets executed using the variables that have been created in the creation phase.

Execution Stack

The Execution Stack, also known as call stack, stores the execution contexts in a stack according to their creation time.

When scripts load in the browser, the Global Execution Context is created as the default context and is placed at the bottom of the execution stack.

In the execution phase when the code is being executed, whenever there is a function call, a new execution context is created for that function and pushed onto the top of the execution stack. The new execution context has its own space for variables and functions. It will go through the creation and execution phases.

The execution context which is on top of the execution stack is the execution context of the current executed code.

After the execution of the function, its execution context is popped out of the stack and the control moves to the execution context below it.

Let us understand the execution stack using an example.

function b() {
    var x = 10;
    console.log(x);  // 10
}

function a() {
    var y = 5;
    console.log(y);  // 5
    b();
}

var z = 1;
a();

After the script is loaded into the JS engine, the JS engine creates the Global Execution Context and places it at the base of the execution stack.

Once function a() is called, the execution context for a is created and pushed onto the stack and it proceeds to the execution phase.

In function a(), variable y is initialized to 5 and printed to the console. After that function b() is invoked and the execution context for b is created. It is pushed onto the stack and function b is executed now.

Now inside function b, variable x is created and logged into the console. Since there are no more statements to execute in function b, the execution phase of function b is completed. The execution context of function b will be popped out of the stack. Hence a's execution context will now be in active state.

As there are no more statements in a after invoking b, a's execution context will also be popped out of the call stack. The global execution context will now be in active state and executes whatever statements are present after calling function a. In our case we have no more statements.

Scope Chain

As we discussed above scope chain is created in the creation phase of the execution context. Scope determines how accessible a piece of code is to other parts of the codebase.

Lexical scope is the ability for a function to access variables from the parent scope. We call the child function to be lexically bound by that of the parent function.

Whenever we try to access a variable, the JavaScript engine checks for the variable in the current execution context. If the variable is not found then it lexically checks in the outer environment's execution context. The outer environment might be somewhere below in the execution stack and the outer environment of current function depends on where the function is located physically in the script.

This process of looking for a variable in outer environment(recursively) until the variable is found or until it reaches global execution context is called "Scope Chain".

Let us understand Scope Chain with an example.

function first(){
    var y = "love";
    function second(){
        var z = "JavaScript";
        console.log(x,y,z);
    }
    second();
}

var x = "I";
first();

In the example above we can see two nested functions. function second is defined inside function first. function second is trying to print variables x, y and z to the console.

But second only has variable z defined inside it. So ideally it should throw ReferenceError when it tries to access x and y. However, due to lexical scoping, it has access to the scope of the function it sits in and that of its parent.

When JavaScript tries to execute the console statement it first tries to find the variable x in the execution context of function second. Since x does not exist in second, it tries to look in its parent function which is first. Even function first does not have variable x in its scope so it now checks in the GEC and finds variable x in GEC.

Same is the case with variable y, javascript engine will be able to find y variable in second's immediate parent function which is first.

This is how the scope chain works in javascript.

So the final output we get is "I love JavaScript".

Thank you for reading!

Please leave your valuable feedback in the comment section.