DEV Community

Cover image for A Comprehensive Guide to How JavaScript is Executed.
Frank Otabil Amissah
Frank Otabil Amissah

Posted on • Edited on

A Comprehensive Guide to How JavaScript is Executed.

JavaScript is a powerful programming language used extensively for web development. However, in order to understand how JavaScript is executed in various engines it is important to understand the various components that come together to create a functional JavaScript environment.

In this article, you'll learn how variables are hoisted and the processes involved at the execution phase.

Without further ado, let's dive in.

Execution Context.

JavaScript codes are executed in contexts. An execution context can be thought of as a container that holds all the necessary information about the current state of the program. This includes variables, functions, and objects that are currently available within the current scope. The first execution context created is the global execution context. The global execution context contains all variables and functions declared in the global scope (window object in web browsers and global object in Node.js). The function and variable declarations are bound to the global object using the this keyword and are then stored in a memory heap.

Hoisting of declarations.

The first thing that happens in the execution contexts is hoisting. This is the process of moving all variable and function declarations to the top of their respective scopes. This means that a variable's value or function can be used or called, respectively, before they're declared later in the code.

For example, suppose we have the following code:

let myVar = "Hello "

function fn(){
    console.log(myVar + "John")
}

fn(); // Returns Hello John
Enter fullscreen mode Exit fullscreen mode

JavaScript creates the global object(wrapper) to encapsulate the code and binds all functions and variables to its this keyword. Now, this example is very straightforward, the variable is at the top so it's hoisted and initialized before any other line is executed.

Let's look at the same example in a different way.

let myVar = "Hello "

fn(); // Returns Hello undefined

var lastname = "John"

function fn(){
    console.log(myVar + lastname)
}
Enter fullscreen mode Exit fullscreen mode

Notice that the function fn is called before it's declared in our code, what happens is that, JavaScript first pulls all variable and function declarations to the top of the scope and initializes them into the memory heap it had created and then proceeds to execute the code line by line (from top to bottom).

Also notice that the function returns Hello undefined instead of throwing an error, that's because only declarations are hoisted and a preinitialized to undefined

However, for variables declared using the let and const keywords their values are not initialized, which means trying to use them before their line executes will throw an error,

For example, suppose we have the following code:

fn(); // Returns Cannot access 'myVar' before initialization

function fn(){
    console.log(myVar + "John")
}

let myVar = "Hello "
Enter fullscreen mode Exit fullscreen mode

Notice the error message "Cannot access 'myVar' before".

JavaScript after hoisting variables local to its scope initializes them to undefined, but for let and const variables their values are left uninitialized till the line of declaration has been reached.

It's important to note that, JavaScript engines prevent the execution of any line of code that is yet to be initialized.

Note: All variables except ones declared by let and const are initialized to undefined including function expressions.

Here are examples of function expressions,

let fn_expression = () =>{
    console.log("I'm a function expression")
}

let fn_expression1 = function() =>{
    console.log("I'm too")
}

var fn_expression2 = function fn() =>{
    console.log("I'm just like the others")
}
Enter fullscreen mode Exit fullscreen mode

Funcion Execution Context.

Every time a function is called, a new function execution context is created inside of the global execution context to handle that function call. It follows the same process as the global execution context by creating an argument object (wrapper). The argument objects has information about the local variables of that function, arguments passed to the function and its scope chain.

Function execution contexts may contain another function execution context, to handle this chain JavaScript engines use a call stack.

Call Stack

The call stack is an essential component of the JavaScript runtime environment that helps keep track of the current execution context. It is a data structure that manages the order in which functions are called and executed in the program.

Every time a function is called in JavaScript, its execution context is added to the top of the call stack. This execution context includes information about the function, such as its arguments, local variables, and scope. Once the function completes its execution, its execution context is removed from the top of the call stack, and the control is passed back to the calling function (the function that called it).

The call stack follows the Last-In-First-Out (LIFO) principle, which means that the most recently added item is the first one to be removed. This means that the functions that are called later are added to the top of the call stack, and the functions that are called earlier are at the bottom of the stack.

For example, suppose we have the following code:

function multiply(a, b) {
  return a * b;
}

function square(n) {
  return multiply(n, n);
}

console.log(square(2));

Enter fullscreen mode Exit fullscreen mode

When the program starts running, the global execution context is added to the call stack. When the square function is called with an argument of 2, its execution context is added to the top of the call stack. Inside the square function, the multiply function is called with the argument 2, and its execution context is added to the top of the call stack.

Once the multiply function completes its execution and returns a value, its execution context is removed from the top of the call stack, and the control is passed back to the square function. Once the square function completes its execution and returns a value, its execution context is removed from the top of the call stack, and the control is passed back to the global execution context.

If the call stack becomes too large, it can cause a stack overflow error. This error occurs when the call stack exceeds its maximum size, typically due to an infinite recursive function.

So far I have talked about how synchronous tasks are handled on the call stack. Now let's look at how asynchronous tasks are handled.

Call Queue

JavaScript is a single-threaded language, which means that it can only execute one task at a time. However, it is often necessary to perform asynchronous tasks such as fetching data from a server or waiting for user input. In order to handle these tasks, the JavaScript environment maintains a call queue. Any functions that need to be executed asynchronously are added to the call queue.

When an asynchronous task is initiated, such as a timer or an AJAX request, it is added to the call queue instead of the call stack. The call queue manages the order in which these tasks are executed, ensuring that they are executed in the correct order.

The call queue follows the First-In-First-Out (FIFO) principle, which means that the tasks that are added to the queue first are the first ones to be executed.

For example, suppose we have the following code:

console.log('First');

setTimeout(function() {
  console.log('Second');
}, 1000);

console.log('Third');

Enter fullscreen mode Exit fullscreen mode

When the program starts running, the console.log('First') statement is executed and added to the call stack. Next, the setTimeout function is called, and a timer is created for one second. Since the setTimeout function is asynchronous, it is added to the call queue instead of the call stack. The console.log('Third') statement is then executed and added to the call stack.

After one second, the timer expires, and the callback function is added to the call queue. Since the console.log('Third') statement has already been executed, the callback function is the next task to be executed in the call queue. The callback function is then added to the call stack and executed, resulting in the output of Second.

Tasks added to the call stack from the call queue are handled by an event loop.

Event Loop

When an event is triggered in the program, such as a user clicking a button or a timer expiring, it is added to the call queue. The event loop continuously checks the call queue for tasks that need to be executed. When a task is found, the event loop checks to see if the call stack is empty, when it is, the event loop takes the task from the call queue and adds it to the call stack for execution.

The event loop operates continuously, checking the call queue for tasks and executing them one by one until the call queue is empty.

For example, suppose we have the following code:

console.log('First');

setTimeout(function() {
  console.log('Second');
}, 0);

console.log('Third');
Enter fullscreen mode Exit fullscreen mode

When the program starts running, the console.log('First') statement is executed and added to the call stack. Next, the setTimeout function is called, and a timer is created for zero seconds. Since the setTimeout function is asynchronous, it is added to the call queue instead of the call stack. The console.log('Third') statement is then executed and added to the call stack.

Since the timer for the setTimeout function is set to zero seconds, the callback function is immediately added to the top of the call queue. The event loop then takes the callback function from the call queue and adds it to the call stack. The callback function is then executed, resulting in the output of Second.

Wrapping Up

In this article, you've learned the steps involved when executing JavaScript code in a runtime environment starting from the creation of scope objects, hoisting of scope variables, and understanding how synchronous and asynchronous tasks are handled by the call stack and call queue.

Thanks for reading. Happy coding!

Top comments (0)