Decoding the Javascript Code Execution: A Deep Dive

Photo by Olga ga on Unsplash

Decoding the Javascript Code Execution: A Deep Dive

·

7 min read

Introduction

Javascript can be a complex language to wrap your head around, especially if you are a newbie or, on a streak, watching video after video, and not popping your head under the hood every once in a while to understand all the nitty-gritty details of what's going on in the background.

This is me before I understood how javascript works

and, this is me now

What!? I still run into trouble sometimes. There is still a lot to learn. So without further ado, let's get started.

How The Execution Starts

For the execution to start, JavaScript needs two primary components:

  • A parser

  • A JavaScript Engine

Parser - A parser is software that reads and analyzes the code line by line to understand the structure of the program. It goes through every line to understand all the function and variable declarations.

Javascript Engine - A javascript engine is needed to efficiently execute the instructions written in code. It works alongside the parser to prepare the code for execution, then in a later stage, execute it. There are different JavaScript engines in the market. Chrome uses the V8 engine. For Safari it is JavaScriptCore. Mozilla Firefox uses SpiderMonkey.

The javascript engine carries out the process of converting the code to machine code and executing it. It includes two components which are the heap memory and the call stack, where the complete execution process takes place.

Let's look at how the entire execution process takes place.

The Execution Context

The javascript file is linked inside the HTML file we have created. When the browser's engine encounters the script tag inside the file, it starts converting the javascript code into a format, the browser can understand. The code is first translated to machine code, then the execution starts.

After the code has been converted, the actual magic beings. In the first step, the browser creates an Execution Context. An execution context is an environment that has all the necessary data, required to execute a piece of code. This includes all the variable declarations, function declarations, and arguments that have been passed to the function. The arguments are accessible via the arguments[] object.

In an execution context, the code gets parsed line by line, and memory is assigned to all the functions (objects) and primitives. It also decides the scope of variables that are specified in the code. A scope is a region where a particular variable or a function can be accessed.

The Creation of Execution Context

The creation of an execution context occurs in two phases:

  1. The memory creation phase

  2. The code execution phase

The Memory Creation Phase

In the memory creation phase, the whole code is scanned and memory spaces are set up for variables and function declarations. In this phase, variables and function declarations are defined as properties on a special object known as a Variable Object.

Variable Object

It is a unique object that contains all the variable and function declarations defined on it as key-value pairs.

When the code is scanned, the variable and function names are created as keys on the variable object with the initial value of variables set to undefined while functions are assigned their whole code. Consider this example:

var $num1 = 10;
var $num2 = 20;

function $sum(a,b){
    return a + b;
}
console.log($sum(30,40));
// The only reason I'm using '$' to create variable names is so that they appear on the top in the variable object. This is because browsers arrange the properties on objects in alphabetical order

To see the Variable Object, one can inspect the code in the browser, then go into the sources section, and put a debugger at various points.

In the bottom-right corner, you can observe that the variables are assigned undefined during the memory creation phase, and the function declaration is given the full function code.

Scope Chain

The second step in the memory creation phase is the creation of a scope chain. A scope chain is a chain-like structure that forms when a function is created inside of another function.

The scope chain determines the accessibility of functions and variables. Whenever a function is called, a new execution context is created with all the variables and function declarations. A child function can access all the variables and functions defined in its parent. This is possible due to the scope chain.

Whenever a new execution context is created, a reference to its parent is passed to it. This series of references form a chain-like structure which is known as the scope chain. Whenever a variable is needed, it is first looked up inside the current execution context, if is it not found, then the search moves to its parent. This process continues until the chain reaches its end in the global scope where either the value is encountered or a ReferenceError is thrown.

For example:

const programName = "Apollo";

function constructString(){
    const lastFlightDate = "7 December 1972";
    console.log(`${programName} Mission's last flight was on ${lastFlightDate}.`);
} 
// Apollo Mission's last flight was on 7 December 1972.

This function requires the programName variable to log the string to the console. First, the variable is looked for in the current scope since console.log() function is defined inside the constructString function. When the variable is not found in the current scope, it is looked for in its parent's scope through the scope chain.

The this Keyword

The third step is the initialization of the this keyword. It is a special keyword whose value is dynamic in nature. This means that it takes the value of the function's owner or variable, inside which this has been used.

For example :

var birthYear = 1988;
var personName = "Raj";

function constructString(){
    console.log( `${this.personName} was born in the year ${this.birthYear}`);
}  
// "Raj was born in the year 1988"

In this example, since the function has been invoked inside global scope, the this keyword refers to the window object. Let's take an example where a function has been created as a method on an object.

const foodOrder = {
    topping: "olives",
    crust: "pan",
    makePizza: function(){
        return `Make a pizza with ${this.crust} crust, topped with ${this.topping}`
    }
}
console.log(foodOrder.makePizza());
// Make a pizza with pan crust, topped with olives.

In this example since the owner of the function is the foodOrder object, the this keyword will point to the object.

  1. Only function expressions and function declarations get their this keyword. Arrow functions don't.

  2. For this to work, make sure you create variables using var since let and const do not create properties on the global object.

The Code Execution Phase

After the creation phase, the code starts executing. Every program needs to be converted to machine code for it to be executed. After the compiler converts the code to machine code, the code starts executing.

The first step to execution is the creation of a Global Execution Context. All the top-level code ( i.e., any code that is not inside a function) starts executing. The global execution context is pushed into a stack data structure known as call stack inside the javascript engine.

As soon as a function is encountered during the execution of the program, a new execution context for it is created, called the Functional Execution Context. The functional execution context is pushed on top of the global execution context in the call stack and its execution starts. Meanwhile, the execution of the global execution context is paused, and will only resume when the functional execution context is popped off from the stack.

When the global execution context finishes its execution, it is removed from the stack, leaving it empty. The stack then awaits any callback functions, if present. Once all callback functions have completed their execution, the code finishes running and the result is produced.

Conclusion

However, this overview provides a basic understanding of the underlying process, there is a lot more than what appears to the eyes. Having an understanding of the fundamentals of execution such as the working of the call stack, heap memory, and execution context, can help us write better and more efficient code leading to more predictable results down the road.