Explaining JavaScript's Execution Context And Stack

Explaining JavaScript's Execution Context And Stack

This article was first published on the Open Replay Blog

With each new abstraction of the Javascript language, which can be in the form of a library or framework, Writing Javascript has become more declarative than imperative. Which is actually a good thing right, things now gets done faster and easier. However, no matter which framework or tool we use to write Javascript code, in the end, our code is still executed as vanilla Javascript by the Javascript engine.

This is why as a Javascript developer you should have a basic understanding of how Javascript works under the hood. Trust me this will make you a more confident and better Javascript developer.

 

INTRODUCTION

This article is for everyone who writes Javascript code, from the beginner to the senior Javascript developer at Netflix deploying production-ready code today(you will still learn a thing or two).

At the end of this article, You will know what the Execution Context and Stack is, how the Execution Context is created and what it does.

 

TABLE OF CONTENTS

  • WHAT IS THE EXECUTION CONTEXT
  • TYPES OF EXECUTION CONTEXT
  • WHAT IS THE EXECUTION STACK
  • HOW THE EXECUTION CONTEXT IS CREATED
  • UNDERSTANDING HOW CODE IS EXECUTED THROUGH THE EXECUTION CONTEXT
  • THE REASON BEHIND HOISTING IN JAVASCRIPT
  • CONCLUSION

 

WHAT IS THE EXECUTION CONTEXT

Imagine your code is placed inside a box whenever you write Javascript code. That box is the Execution context.

The Execution Context is a conceptual environment created by Javascript for code evaluation and execution.

 

TYPES OF EXECUTION CONTEXT

There are 3 types of Execution Context in javascript.

  1. The Global Execution Context (GEC): This is the primary or base Execution Context. It is the highest level of abstraction for an execution context in javascript.

    It has two main functions. i. Create a global object. ii. Attach the this value to the global object

  2. The Function Execution Context (FEC): An execution context is created for every function invocation. It is not created when the function is declared, only when called or invoked. When a function is called it is executed through the execution stack.
  3. The eval() Execution Context: Due to the malicious nature of eval(), it is rarely used in javascript, So it won’t be talked about here. However, an execution context is created whenever it is used.

 

THE EXECUTION STACK (Call Stack)

A stack is a data structure that follows the Last in First Out(LIFO) principle. However, the execution stack is a stack that keeps track of all the execution context created during code execution.

The 3 step process of the execution stack

STEP 1: The global execution context is initially added(pushed) on the execution stack by default. In the form of the global() object.

STEP 2: A Function execution context is added to the stack when a function is invoked or called.

STEP 3: The Invoked function is executed and removed(popped) from the stack, along with it’s execution context.

Example


function greeting() { 
   sayHi();
}

function sayHi() {
   return "Hi!";
}

// Invoke the `greeting` function
greeting();

STEP 1: The GEC is created and pushed on the execution stack as the global() object.

STEP 2:

  1. The greeting() function is invoked and pushed on the stack.
  2. The sayHi() function is invoked and pushed on the stack.

STEP 3:

  1. The sayHi() function is popped off the stack.
  2. The greetings() function is popped off the stack.

 

HOW THE EXECUTION CONTEXT IS CREATED

The execution context is created in two stages:

  1. Creation stage
  2. Execution stage

CREATION STAGE A lexical environment is created in the creation stage of the execution context. It is of two types:

  1. Lexical Environment: This is the main or primary Lexical Environment.
  2. Variable Environment: This is a type of Lexical Environment used specifically by the var VariableStatement within an Execution Context.

 

Conceptual Representation:

ExecutionContext = {

LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
VariableEnvironment = <ref. to VariableEnvironment in memory>  

}

 

Lexical Environment According to the official ES6 docs.

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.

The lexical environment is a standard or specification type. It defines the identifier that represents a specific variable or function, based on its lexical nesting structure or scope. The lexical environment is simply a structure that contains identifier variable mapping.

Example

let x = 10;
let y = 20;

function greeting(){
  console.log('Hello');
}

The lexical environment for the preceding code will look like this:

lexicalEnvironment = {
  x: 10,
  y: 20,
  greeting: <ref. to greeting function>
}

The Lexical Environment is made up of three components:

  1. Environment Record
  2. Reference to an outer environment
  3. The this binding

 

Environment Record According to the official ES6 docs: An Environment Record records the identifier bindings that are created within the scope of its associated lexical environment.

The Environment Record is simply the location where variable and function declarations are stored in a lexical environment. It is located in the memory heap of the Javascript engine.

The Environment Record is of two types:

  • Declarative Environment Record: This is mainly used by a Lexical Environment created in the Function Execution Context. It records(store) function declarations and variable declarations. The Declarative Environment Record also stores an arguments object, which contains the length(number) of argument(s) passed to a function, and a map of the argument(s) and its index.

Example

function add(a,b) {
   let c = a + b;
}

add(10,20);

// argument object
Arguments: {0: 10, 1: 20, length: 2}
  • Object Environment Record: This is mainly used by a Lexical Environment created in the Global Execution Context. It stores function declarations, variable declarations, and a global binding object(window object in browsers).

 

Reference to an outer Lexical Environment Every (inner) Lexical Environment reference an outer Lexical Environment. The outer Lexical Environment logically surrounds the inner Lexical Environment that it is referenced by. The Javascript engine can look for variables in the outer Lexical Environment if they are not found in the inner Lexical Environment.

N.B— The Lexical Environment in the Global Execution Context has no reference to an outer Lexical Environment. It is referenced to null.

Example

function A () {

  function B () {
  // some code
  }

  function C () {
  // some code
  }

// some code
}

In the preceding code function A contains two nested functions function B and function C. Since function B and function C are nested in function A. Therefore, the Lexical Environment of function A will be the outer Lexical Environment that is referenced by the inner Lexical Environments of both function B and function C.

 

The this Binding The this binding component is where the value of this is set.

The value of this is set to the global object(i.e window object in browsers) in the Global Execution Context. However, in the Function Execution Context, this value depends on how the function is called. If the function is a method in an object, this value of the function is set to that object when called by the object reference. Otherwise, it is set to the global object.

Example

const company = {
  name: 'facebook',
  yearFound: 2004,
  getAge: function() {
    const date = new Date();
    let currentYear = date.getFullYear();
    console.log(currentYear - this.yearFound);
  }
}
company.getAge();

const companyAge = company.getAge;
companyAge();

In the preceding code, when the company.getAge() method is called this will refer to company object. However, when companyAge() is called, this refers to the global object.

 

Conceptual Representation of the Lexical Environment:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outerEnv: <null>,
    this: <global object>
  }
}

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outerEnv: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}

 

Variable Environment

The Variable Environment has been explained earlier in this section, as a type of Lexical Environment used specifically by the var VariableStatement within an Execution Context. This means that the Environment Record of the VariableEnvironment only records bindings created by the var VariableStatement. Whereas the Environment Record of the LexicalEnvironment records both variable (let and const) bindings and function declarations.

 

EXECUTION STAGE In this, the stage variables are assigned to their respective values and the code is executed.

 

UNDERSTANDING HOW CODE IS EXECUTED THROUGH THE EXECUTION CONTEXT

Now let’s understand how an actual JavaScript code goes through the two stages of the Execution Context.

Example

let a = 10;
const b = 20;
var c;

function add(e, f) {
   var d = 30;
   return e + f + d;
}

c = add(40, 50);

This is how the Javascript engine will execute the preceding code in the two stages of the Execution Context.

 

CREATION STAGE Here our code is being scanned for variable and function declarations.

Global Code:

In the creation stage, the Javascript Engine creates a Global Execution Context to Execute the global code. Which should look like this.

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
      b: < uninitialized >,
      add: < func >
    }
    outerEnv: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outerEnv: <null>,
    ThisBinding: <Global Object>
  }
}

Function Code:

When the add(40, 50) function is called, a new Function Execution Context is created to execute the function code. The Function Execution Execution Context will look like this in the creation stage.

FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 40, 1: 50, length: 2},
    },
    outerEnv: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      d: undefined
    },
    outerEnv: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

 

EXECUTION STAGE Here values are assigned to variables.

Global Code:

In the Execution Stage when variable assignments are done. This is what the Global Execution Context will look like.

GlobalExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 10,
      b: 20,
      add: < func >
    }
    outerEnv: <null>,
    ThisBinding: <Global Object>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      d: undefined,
    }
    outerEnv: <null>,
    ThisBinding: <Global Object>
  }
}

Function Code:

After the Global Execution Context has gone through the execution stage, assignments to variables are done. This also includes variables inside every function declaration in the Global Execution Context. The Function Execution Context will look like this during the execution stage.

FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 40, 1: 50, length: 2},
    },
    outerEnv: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      d: 30
    },
    outerEnv: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

The program finishes after the add() function is executed and returns a value that is stored inside c.

 

THE REASON BEHIND HOISTING IN JAVASCRIPT

From the preceding section, we know that, during the creation stage when our code is scanned for variable and function declarations. The function declaration is stored completely in memory, while variables are initially set to undefined(for var declaration) or remain uninitialized (for let and const declarations).

This is the reason why variables defined with var can be accessed before they are declared, though you will get a value of undefined . However, this is not the case for variables defined with let and const, which will throw a reference error when they are accessed before being declared. This is known as hoisting in Javascript, which is simply being able to access a variable before it is declared.

 

CONCLUSION

Now you know how the Javascript Engine Executes your code internally through the Execution Context. Learning concepts like this will make you confident as a developer and also make it easy to learn more fundamental concepts in Javascript like scopes and closures.

  Summary In this article you learned:

  • The Execution Context is an abstract environment created by Javascript to execute code.
  • The Execution Stack is used to execute functions and keep track of all the Execution Context created.
  • The Execution context is created in two stages, the creation and the execution stage.
  • The Lexical Environment is a core part of the creation stage.
  • Variable assignments are done in the execution stage.
  • Hoisting is being able to access a variable before it is declared.