Execution Context in JavaScript

Published on 9th May, 2020 9 minutes read javascript

Topic of discussion today is "execution context", an abstract concept that holds information about the environment within which the current code is being executed. Very often, programmers use scope and execution context interchangeably but in reality, they are very different concepts. Misunderstanding them can result into very frustrating situations. In this article I will try my best to clear things up and send you in your merry way.

Introduction

First thing first, scope and execution context are two very different concepts. Execution context is the environment in which the your code is executed. Whereas scope defines which variables and functions you have access to, depending on where the execution is happening (a physical position) within your code.

Depending on the block level of execution, the scope can change but multiple blocks of code can all be executed within the same execution context.

Let me show you an example:

var globalThis = this;

function func1() {
    var num = 5;

    console.log(`x: ${x}`);
    console.log('Global This: ', globalThis);
    console.log('Local This: ', this);
    console.log('Are they same?', globalThis === this);
}

function func2() {
    var num = 10;

    console.log(`x: ${x}`);
    console.log('Global This: ', globalThis);
    console.log('Local This: ', this);
    console.log('Are they same?', globalThis === this);
}

func1();
func2();

// num: 5
// Global This: Window
// Local This: Window
// Are they same? true
// num: 10
// Global This: Window
// Local This: Window
// Are they same? true

In this code snippet, there are two block levels. One defined by func1 and the other defined by func2. Both of them have a variable num set to different values. Once executed, you can see that value of num is different depending on the scope but the value of this is same across all scopes.

Although scope and execution are are different concepts, scope plays an important role in how the execution context is defined.

Definition and Types of Execution Context

Put simply, an execution context is an abstract concept of an environment where the JavaScript code is evaluated and executed. It holds information like the value of this, variables, objects, and functions JavaScript code has access to at a particular time.

There are three types of execution context:

  • Global Execution Context.
  • Functional Execution Context.
  • Eval Execution Context.

Allow me to provide you with some more details.

Global Execution Context

The global execution context is the the default or base execution context. Code that is not inside any function is in the global execution context. It creates a global object (the window object in the case of browsers) and sets the value of this to that object. Global execution context gets created automatically without any function call or anything. There can be only one global execution context in a code.

Functional Execution Context

Functional execution context on the other hand is created whenever a function is invoked. Each function has its own execution context. There can be multiple functional execution contexts.

Eval Execution Context

Eval execution context exists inside eval function calls but as it's not used by JavaScript programmers thus I'll skip that.

To understand how an execution context is created, you need to see behind the scenes of the JavaScript engine.

Inside The JavaScript Engine

In one of my previous articles on Hoisting in JavaScript, I briefly talked about what goes on behind the scenes. What I told was that the JavaScript engine does it's job of reading and executing code in two phases:

  • Memory Creation Phase.
  • Execution Phase.

I also took a high level look at the Memory Creation Phase. Memory creation phase plays a huge role in the creation of execution context as well. During this phase three things happen:

  • Creation of scope.
  • Creation of scope chain.
  • Determination of the value of this.

Knowing at least a little bit about these three is important specially the last one.

Creation of Scope

In this step, an activation object is being created. An activation object is the object used to hold all the variables, function arguments and inner functions declaration information. Hoisting takes place in this step. The engine scans through the code looking for variable, function declarations and put them into memory. It's necessary for a execution context to know about the variables and functions it has access to.

Creation of Scope Chain

Every execution context has a reference to its outer scopes, all the way up to the global scope. This chain of reference is what we call a scope chain. Think of it as a linked list of outer scopes. One important thing to know is that, a scope chain doesn't contain any information about sibling scopes withing it's execution context. You can access global scope from local scope but the opposite is not applicable. Also accessing a local scope information form another local scope is not possible.

Determination of the value of this

Every execution context has it's own this variable. It points to an object that owns the current code in execution. In this step the engine figures out which object owns the code that's being executed now and assigns that to this.

You've seen this briefly in a previous example, here is a shortened version of that code:

var globalThis = this;

function func1() {
    console.log('Global This: ', globalThis);
    console.log('Local This: ', this);
    console.log('Are they same?', globalThis === this);
}

func1()

// Global This: Window
// Local This: Window
// Are they same? true

In this example, the value of this in global execution context is set to Window object. The value of this inside the functions is set to the same Window object as well.

Let me explain how this gets it value under the hood.

Value of this

Try to guess the output:

var globalThis = this;

var obj1 = {
    innerMethod: function() {
        console.log('Global This: ', globalThis);
        console.log('Local This: ', this);
        console.log('Are they same?', globalThis === this);
        console.log('Is this the object?', obj1 === this);
    }
};

obj1.innerMethod();

// Global This: Window
// Local This: Object { innerMethod: innerMethod() }
// Are they same? false
// Is this the object? true

This is a slightly modified version of the previous code snippet you saw. All I did is, putting the function inside an object and inner this starts pointing to that leading parent object.

Explanation for what just happened is that this gets assigned to the leading parent object of a function call at the time of execution. If there isn't a parent object, value of this gets assigned to the global object. Remember the at the time of execution part though, it's going to complicate things for you soon.

In the previous code snippet, the function was not inside any object. Hence, the value of this defaulted to the global object Window.

In the later code snippet however, the function has a preceding reference to an object and value of this within the execution context of innerMethod call was set to obj1.

Invoke Time and Nested Functions

Let me show you a more complicated example:

var obj1 = {
  innerMethod: function () {
    console.log(this);
  }
};

var func1 = obj1.innerMethod;

func1();

// Window

What I am doing here is assigning a reference to obj1.innerMethod to another variable called func1. Theoretically the value of this should point to the leading parent object but as you can see it points to the Window object.

I hope that you remember me saying that, this gets assigned to the leading parent object of a function call at the time of execution. So as you can see, at the time the function was invoked, it did not have a preceding parent object. This is why this defaulted to Window.

Look at this snipped with nested functions:

var obj1 = {
  innerMethod: function () {
    innerFunction();

    function innerFunction() {
        console.log(this);
    }
  }
};

obj1.innerMethod();

// Window

Again the value of this defaults to Window as the innerFunction doesn't have a leading parent object.

Controlling this

So far, it seems like that controlling the value of this is not within the programmer's hand. Specially in nested situations where you can clearly see there is a preceding object.

There are some ways to go around this problem though:

self Variable

One way to solve this issue is to save a reference to this at assignment time:

var obj1 = {
  innerMethod: function () {
    var self = this;
    innerFunction();

    function innerFunction() {
        console.log(self);
    }
  }
};

obj1.innerMethod();

// Object { innerMethod: innerMethod() }

By saving the value of this inside another variable self the value can be retained for later use.

bind(), call() and apply() Functions

bind, call and apply are three in-built methods available to any function in JavaScript.

All of them essentially does the same thing, taking an argument and setting it as the parent object of the execution context. There is a a slight difference though.

bind returns a function, which when invoked later will have a set context, call and apply on the other hand immediately invokes a function with a set context.

var obj1 = {};

function func1() {
    console.log(this);
}

func1();
func1.bind(obj1)(); // bind returns a function
func1.call(obj1);
func1.apply(obj1);

// Window
// Object {  }
// Object {  }
// Object {  }

bind, call and apply are defined in JavaScript's function prototype.

this in ES6 Arrow Functions

In ES6, a new syntax to create functions was introduced. The arrow functions. Here's an example:

'use strict'

let func1 = () =>  {
  console.log(this);
};

func1();

// Window

If you want to learn about the basics of arrow function go here.

The value of this works quite differently inside arrow functions. Here's an example:

const obj1 = {
  innerMethod: () => {
    console.log(this);
  }
};

obj1.innerMethod();

// Window

If you thought the output to be Object { innerMethod: innerMethod() }, I won't blame you, after all that's what you've been seeing till now.

Explanation for this is that arrow functions use what we call a lexical scope which essentially means that arrow functions do not have their own value of this rather they inherit from the parent. Thats' the value of this in the above code snippet is Window.

Here is a slightly modified version of the snippet. Have a look:

const obj1 = {
  innerFunction: null,
  innerMethod: function () {
    this.innerFunction = () => { console.log(this) };
  }
};

obj1.innerMethod();
obj1.innerFunction();

// Object { innerFunction: innerFunction(), innerMethod: innerMethod() }

Built-in functions such as bind, call and apply doesn't work in arrow functions:

const obj1 = {};

const func1 = () => {
    console.log(this);
}

func1();
func1.bind(obj1)(); // bind returns a function
func1.call(obj1);
func1.apply(obj1);

// Window
// Window
// Window
// Window

Arrow functions are powerful addition to the language but they are not silver bullets. In my day to day work, I constantly find places where arrow functions are not usable and it can get very frustrating if you don't know how they work.

In my opinion, arrow functions are the best choice when working with callbacks, but not a good choice when working with class/object methods or constructors.

Execution Stack

I hope after all that I've explained so far, you now understand what execution context is and how it gets the value of this, variables and functions during memory creation phase. You should also know that the global execution context gets created automatically and there can be only one global execution context.

And functional execution contexts are created whenever a function is invoked. So rule of thumb is:

  • function call = new execution context

Now assume that you have two function calls in your code. You will have three execution contexts in total, including the global execution context. To see how they all work in harmony, let me take you deeper into the rabbit hole, the execution stack:

The Execution Stack

Like any other stack in programming world it uses LIFO method. So whenever a new execution context is created, it gets placed on top of the previous context. This way the last created execution context is always on the top and the global stack is at the end. Execution stack exists for the JavaScript engine to keep track of the order of execution.

The important thing to know here is that, the JavaScript engine can only be executing within a single context.

When you run the code, it starts off from the global execution context. When a function func1 is called, it goes into the execution context of func1(), and everything that’s happening in the global context is paused, until the JavaScript engine exits the context func1(). If it encounters another execution func2 before finishing the current, another context func2() is created. The JavaScript engine pauses whatever that’s happening in the context func1() and goes inside func2(). As soon as the engine finishes executing the code there, the context func2() is removed from the top of the stack. Only then, does the engine go back to where it left off in the previous context func1(), and resumes its execution. This cycle is repeated until the last line of code is executed in the global execution context.

This is why JavaScript is described as single-threaded, as only one thing can be processed at any given time, and multiple events have to happen in order, one after another.

Conclusion

Among all the complicated concepts in JavaScript, execution context is one of the hardest to explain. I had to go through a lot of articles online before I could write my own. I have tried to make things as clear as I could but if you still have confusions, reach out to me by writing in the comments section.

I hope you'll be a bit more confident when dealing with the value of this from now on.