JavaScript is a single-threaded programming language. This means that JavaScript can do only one thing at a single point in time.

The JavaScript engine executes a script from the top of the file and works its way down. It creates the execution contexts, and pushes, and pops functions onto and off the call stack in the execution phase.

When the JavaScript engine scans a script file, it makes an environment called the Execution Context that handles the entire transformation and execution of the code.

JavaScript Execution Context is the environment in which JavaScript code is executed. It contains information about the variables, functions, and objects that are available to the code being executed, as well as the scope chain and the value of the this keyword.

Execution stack, also known as “calling stack” in other programming languages, is a stack with a LIFO (Last in, First out) structure, which is used to store all the execution context created during the code execution.

There are two types of execution contexts:

  • Global Execution Context (GEC): The Global Execution Context is also called the base/default execution. The global execution context is created when a JavaScript script first starts to run, and it represents the global scope in JavaScript. Each JavaScript file has only one GEC.
  • Function Execution Context (FEC): When a function is called, the JavaScript engine creates a Function Execution Context within the GEC to execute the code within that function. Multiple FECs can exist during script runtime, as each function call has its own FEC. FEC can access the entire code of the GEC, but it is not possible for GEC to access all the code of the FEC. During the GEC code execution.

When Global Execution Context or Function Execution Context is created. Execution of code is happening in two phases:

  • Creation Phase, the JavaScript engine sets up the environment for the code to be executed. During this phase, the JavaScript engine creates the following:
    • Variable Environment: an Environment Record that holds bindings created by var declarations within this execution context.
    • Lexical Environment: an Environment Record used to resolve identifier references, ie. let or const, made by code within this execution context.

      An Environment Record is used to define the association of identifiers to specific variables and functions based upon the lexical nesting structure of the code. Each Environment Record has a reference to the outer environment, which could either point to the Global environment or an outer function environment, enabling the scope chain.

      The Scope Chain is a list of Variable Objects that are accessible in the current scope. Each Variable Object in the Scope Chain represents a higher level of scope.

      There are a few different types of environment records: Global Environment Record, Declarative Environment Record and Object Environment Record.

      The Global Environment Record is a composite environment record, made up of a Declarative Environment Record and an Object Environment Record.

      The Declarative Environment Record is associated with a scope containing variable, const, let, class, module, import, and/or function declarations. A Declarative Environment Record binds the set of identifiers defined by the declarations contained within its scope.

      The Object Environment Record is associated with an object called its binding object. Its binding object is the global object, which is window in the case of browsers.

      For every function declaration, a property is added to the record, pointing to that function, and that property is stored in memory. But function expressions are not included. This means all the function declarations will be stored and made accessible inside the execution context, even before the code starts running. This process of storing variables and function declaration in memory prior to the execution of the code is known as Hoisting.
    • Determining the value of the this keyword: The value of this is determined and stored in the Execution Context. The value of this depends on how the function is called, and can refer to the global context, the current instance of an object (in the case of methods), or be set explicitly using functions like call, apply, or bind.

    In the below code, letter is a binding identifier and the value the variable or binding contains is "a". Note that variables or bindings contain values, they aren't the values themselves. This is important to understand, variables are containers for values.

    const letter = "a"
    var number = 1
    
    function greeting() {
        console.log('Welcome to Javascript')
    }
    
    const person = {
        name: "John Doe"
    }

    The below snippet is a pseudo-code representation of execution context.

    GlobalExecutionContext = {
        LexicalEnvironment: {
            DeclarativeEnvironmentRecord: {
                letter: "a",
                greeting: <ref. to greeting function object>,
                outer: null
            },
            ObjectEnvironmentRecord: {
                window: <ref.to Global object>,
                this: <ref.to window Object>,
                person: {
                    name: "John Doe"
                }
            }
        },
        VariableEnvironment: {
            DeclarativeEnvironmentRecord: {
                number: 1,
                outer: null
            }
        }
    }
  • Execution Phase, the JavaScript engine executes the code line by line. The JavaScript engine reads the code and executes it one line at a time. This phase involves the following steps:
    • Assigning Values to Variables: JavaScript engine assigns values to variables. If a variable is not initialized, it has the value of undefined.
    • Executing Functions and Code Blocks: JavaScript engine executes functions and code blocks as it encounters them in the code. If a function is called, the engine creates a new execution context for that function and adds it to the call stack.
    • Managing the Call Stack: When a function is called, its execution context is added to the top of the call stack. When the function returns, its execution context is removed from the stack.

The two main components of an execution context in JavaScript are

  • Memory Component — This refers to the memory space that is allocated for the code and data components within the context. This includes variables, objects, arrays, and other data structures that are used or manipulated by the code. The memory component is also responsible for maintaining the scope chain, which is a list of variable objects that a function has access to, starting with its own variable object and continuing with the variable objects of its parent functions, all the way up to the global variable object.
  • Code Component — This refers to the actual code that is being executed within the context. It includes any function and variable declarations, as well as any other instructions that make up the code. During the creation phase of the execution context, the JavaScript engine sets up memory space for all variables and function declarations through a process known as hoisting.

These two components enable the JavaScript engine to execute code and manage data within the program.

Let's understand the Execution Context with the following code example below:

function greeting() {
    console.log("Welcome to the JS world!");
}

greeting();

var number1 = 10;
var number2 = 5;

function add(number1, number2) {
    return number1 + number2;
}

function addExtra(number1, number2) {
    var extra = 15;
    return number1 + number2 + extra;
}

var result1 = add(number1, number2);
var result2 = addExtra(number1, number2);

console.log(result1);
console.log(result2);

Using the example code above, let's walk through each stage of what happens during the Memory Creation Phase:

  • Javascript engine will create and push a global execution context into the call stack.
  • Javascript engine will create the this object and bind it to the global object i.e., window in the web browser or global in Node.js.
  • Javascript engine will create a new object called this, which is a reference to the current execution context.
  • Javascript engine will set the value of this to the global object, which is the top-most object in the scope chain. In a browser, the global object is the window object, while in a Node.js environment, it's the global object.
  • Javascript engine will allocate memory for variables number1, number2, result1, result2 and assign undefined to them
  • Javascript engine also allocates memory for function greeting, add, addExtra which includes space for the function's code, its name, and its scope.
  • The below snippet is a pseudo-code representation of global execution context during the creation phase:
    GlobalExecutionContext = {
        LexicalEnvironment: {
            DeclarativeEnvironmentRecord: {
                greeting: <ref. to greeting function object>,
                add: <ref. to add function object>,
                addExtra: <ref. to addExtra function object>,
                outer: null
            },
            ObjectEnvironmentRecord: {
                window: <ref.to Global object>,
                this: <ref.to window Object>
            }
        },
        VariableEnvironment: {
            DeclarativeEnvironmentRecord: {
                number1: undefined,
                number2: undefined,
                outer: null
            }
        }
    }

After the memory creation phase is completed, the Javascript engine moves on to the execution phase. The JS engines scan over the function in the code one more time, updating the variable object with the values of the variables and then running the code, which is known as an execution phase.

Let's look at what happens during the execution phase:

  • greeting function is called, Javascript engine will create a new local execution context for it and push it to the top of the call stack.
  • The greeting function execution context enters the Creation Phase.
  • Javascript engine will create arguments object in the function's local memory with a length of 0 as its starting value.
  • Javascript engine will create this object and bind to the global object.
  • Here is a pseudo-code representation of greeting function execution context during the creation phase:
    GreetingFunctionExecutionContext = {
        LexicalEnvironment: {
            DeclarativeEnvironmentRecord: {
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            },
            ObjectEnvironmentRecord: {
                arguments: {
                    length: 0
                },
                this: <ref.to window Object>
            }
        },
        VariableEnvironment: {
            DeclarativeEnvironmentRecord: {
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            }
        }
    }
  • The greeting function execution context enters the Execution Phase.
  • Javascript engine will display "Welcome to the JS world!" to the console.
  • Here is a pseudo-code representation of greeting function execution context during the execution phase:
    GreetingFunctionExecutionContext = {
        LexicalEnvironment: {
            DeclarativeEnvironmentRecord: {
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            },
            ObjectEnvironmentRecord: {
                arguments: {
                    length: 0
                },
                this: <ref.to window Object>
            }
        },
        VariableEnvironment: {
            DeclarativeEnvironmentRecord: {
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            }
        }
    }
  • The greeting function execution context is returned and the call stack is popped off.
  • Control returns to the global execution context.
  • Javascript engine will assign 10 to number1.
  • Javascript engine will assign 5 tonumber2.
  • add function is called, Javascript engine will create a new local execution context for it and push it to the top of the call stack.
  • The add function execution context enters the Creation Phase.
  • Javascript engine will create arguments object in the function's local memory with a length of 2 as its starting value.
  • Assign the value of number1 to the first index of the arguments object.
  • Assign the value of number2 to the second index of the arguments object.
  • Javascript engine will create number1 and number2 variable in the function's local memory and assignthe corresponding values from the arguments object.
  • Javascript engine will create this object and bind to the window object.
  • Here is a pseudo-code representation of add function execution context during the creation phase:
    AddFunctionExecutionContext = {
        LexicalEnvironment: {
            DeclarativeEnvironmentRecord: {
                number1: 10,
                number2: 5,
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            },
            ObjectEnvironmentRecord: {
                arguments: {
                    0: 10,
                    1: 5,
                    length: 2
                },
                this: <ref.to window Object>
            }
        },
        VariableEnvironment: {
            DeclarativeEnvironmentRecord: {
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            }
        }
    }
  • The add function execution context enters the Execution Phase.
  • The return statement is evaluated.
  • Javascript engine will look up number1, number2 variables in the function's local memory.
  • Javascript engine locates the number1, number2 variables in local memory, then swaps the values to the variable references.
  • The evaluated result of the add function execution context is returned and the call stack is popped off.
  • Here is a pseudo-code representation of add function execution context during the execution phase:
    AddFunctionExecutionContext = {
        LexicalEnvironment: {
            DeclarativeEnvironmentRecord: {
                number1: 10,
                number2: 5,
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            },
            ObjectEnvironmentRecord: {
                arguments: {
                    0: 10,
                    1: 5,
                    length: 2
                },
                this: <ref.to window Object>
            }
        },
        VariableEnvironment: {
            DeclarativeEnvironmentRecord: {
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            }
        }
    }
  • The control returns to its caller context (the global execution context) with the returned value of 15.
  • Javascript engine will assign 15 to result1.
  • addExtra function is called, Javascript engine will create a new local execution context for it and push it to the top of the call stack.
  • The addExtra function execution context enters the Creation Phase.
  • Javascript engine will create arguments object in the function's local memory with a length of 2 as its starting value.
  • Assign the value of number1 to the first index of the arguments object.
  • Assign the value of number2 to the second index of the arguments object.
  • Javascript engine will create number1 and number2 variables in the function's local memory and assign the corresponding values from the arguments object.
  • Javascript engine will create extra variable in the function's local memory and assign undefined to it
  • Javascript engine will create this object and bind to the window object.
  • Here is a pseudo-code representation of addExtra function execution context during the creation phase:
    AddExtraFunctionExecutionContext = {
        LexicalEnvironment: {
            DeclarativeEnvironmentRecord: {
                number1: 10,
                number2: 5,
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            },
            ObjectEnvironmentRecord: {
                arguments: {
                    0: 10,
                    1: 5,
                    length: 2
                },
                this: <ref.to window Object>
            }
        },
        VariableEnvironment: {
            DeclarativeEnvironmentRecord: {
                extra: undefined,
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            }
        }
    }
  • The addExtra function execution context enters the Execution Phase.
  • Javascript engine will assign 15 to extra.
  • The return statement is evaluated.
  • Javascript engine will look up number1, number2, extra variables in the function's local memory.
  • Javascript engine locates the number1, number2, extra variables in local memory, then swaps the values to the variable references.
  • The evaluated result of the addExtra function execution context is returned and the call stack is popped off.
  • Here is a pseudo-code representation of addExtra function execution context during the execution phase:
    AddExtraFunctionExecutionContext = {
        LexicalEnvironment: {
            DeclarativeEnvironmentRecord: {
                number1: 10,
                number2: 5,
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            },
            ObjectEnvironmentRecord: {
                arguments: {
                    0: 10,
                    1: 5,
                    length: 2
                },
                this: <ref.to window Object>
            }
        },
        VariableEnvironment: {
            DeclarativeEnvironmentRecord: {
                extra: 15,
                outer: <ref. to GlobalExecutionContext Lexical Environment>
            }
        }
    }
  • The control returns to its caller context (the global execution context) with the returned value of 30.
  • Javascript engine will assign 30 to result2.
  • Javascript engine will display 15 and 30 to the console.
  • The below snippet is a pseudo-code representation of global execution context during the execution phase:
    GlobalExecutionContext = {
        LexicalEnvironment: {
            DeclarativeEnvironmentRecord: {
                greeting: <ref. to greeting function object>,
                add: <ref. to add function object>,
                addExtra: <ref. to addExtra function object>,
                outer: null
            },
            ObjectEnvironmentRecord: {
                window: <ref.to Global object>,
                this: <ref.to window Object>
            }
        },
        VariableEnvironment: {
            DeclarativeEnvironmentRecord: {
                number1: 10,
                number2: 5,
                result1: 10,
                result2: 30,
                outer: null
            }
        }
    }
  • The global execution context is removed from the call stack or the call stack is popped off.