DEV Community

Cover image for How JavaScript Works: A Technical Exploration of Its Core Processes
Avishka Kapuruge
Avishka Kapuruge

Posted on

How JavaScript Works: A Technical Exploration of Its Core Processes

In the early days of the web, websites were mostly static with simple HTML and limited interactivity. Everything changed in 1995 with the introduction of JavaScript, which allowed for dynamic, interactive, and engaging web applications.

JavaScript was created by Brendan Eich for Netscape 2, and by 1997, it had become the ECMA-262 standard. Designed to enhance the client side of websites, JavaScript brought life to static HTML pages with dynamic and interactive elements.

Today, JavaScript is a versatile and powerful language in many areas, including web and mobile development, server-side applications, game development, and IoT. Its flexibility and the vast ecosystem of frameworks and libraries make it essential for modern software development.

In this article, we'll explore key concepts of JavaScript, such as threads and the call stack, execution contexts, asynchronous operations, memory management, and the JavaScript engine.

Execution Thread & Call Stack

JavaScript is a single-threaded programming language, which means it has a single Call Stack. This means that JavaScript can only execute one piece of code at a time.

Thread

In the context of JavaScript, a "thread" refers to a single sequence of commands or a single path of execution through which JavaScript code is processed. Since JavaScript is single-threaded, it can only execute one task at a time in a specific order. This is part of the reason why asynchronous programming patterns are important in JavaScript for handling operations like network requests, which could otherwise block the thread and make the UI unresponsive.

LIFO Principle in Stack

The Last In, First Out (LIFO) principle is a method of processing data in which the most recently added item is the first one to be removed. This principle is commonly used in data structures like stacks, which are used in various computing algorithms and processes.

LIFO Principle

Call Stack

The Call Stack is a mechanism used by the JavaScript interpreter to keep track of function calls in a script. It operates on the "last in, first out" (LIFO) principle, meaning the last function that gets called is the first to be executed and removed from the stack once its execution completes.

How LIFO Works in the Call Stack

In JavaScript, the call stack uses the LIFO principle to manage function calls:

  1. Function Call: When a function is called, it is added (pushed) to the top of the stack.
  2. Function Execution: The JavaScript engine executes the function on top of the stack.
  3. Function Return: Once the function execution is complete, it is removed (popped) from the top of the stack.
  4. Next Function: The engine then continues executing the next function on the top of the stack.

Call Stack

Execution Context

The JavaScript execution context is a fundamental concept that is critical for understanding how JavaScript code is executed. At a high level, an execution context can be thought of as an environment or a scope in which JavaScript code is evaluated and executed.

Global Execution Context

The Global Execution Context (GEC) is the base execution context in JavaScript that is created when a JavaScript program starts running. It is the outermost context, responsible for managing the execution of the entire script, and all other execution contexts (e.g., function execution contexts) are nested within it. There's only one GEC in a program.

In the GEC:

  • Global Object: It creates a global object (window in web browsers, global in Node.js).

  • this Binding: The this keyword is bound to the global object.

  • Hoisting: Variables and function declarations are hoisted, meaning they are registered in memory before the code execution phase.

Functional Execution Context

The Functional Execution Context (FEC) is created whenever a function is invoked in JavaScript. It is responsible for managing the execution of that specific function, including its variables, arguments, and the value of this. Each function call generates a new execution context that gets pushed onto the call stack.

The creation of an Execution Context (GEC or FEC) happens in two phases:

Creation Phase

In this phase, the JavaScript engine scans the code to be executed. It sets up the memory space for variables and functions, which is known as "hoisting". Variables are initially set to undefined and functions are fully defined. The this keyword is also established.

Execution Phase

In this phase, the code is executed line by line. The variables are assigned their values as the code runs, and functions are executed when their calls are reached.

What is Hoisting?

Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their containing scope before the code has been executed. This means that variables and functions can be used before they are declared in the code. It's important to note that only the declarations are hoisted, not the initialization.

console.log(x); // undefined
var x = 5;
console.log(x); // 5
Enter fullscreen mode Exit fullscreen mode

Asynchronous Operations

Asynchronous programming in JavaScript allows tasks to be executed without blocking the main thread, enabling the handling of multiple operations simultaneously. This approach is essential for maintaining the responsiveness of applications, especially in environments like web browsers where long-running tasks can freeze the user interface.

Blocking Execution

Blocking execution refers to operations that block further execution until the current operation is completed. In other words, the code execution waits for the operation to finish before moving on to the next line of code. This can cause the program to become unresponsive if the operation takes a long time to complete.

Non-Blocking Execution

Non-blocking execution refers to operations that allow the program to continue executing other code while the operation is being performed. This is typically achieved using asynchronous operations, such as callbacks, promises, and async/await. Non-blocking execution helps keep the application responsive, even when performing time-consuming tasks.

Callbacks

A callback is a function passed as an argument to another function, which is invoked after the completion of an asynchronous task.

// function with a callback
function greet(name, callback) {
   console.log(`Hi ${name}`);
   callback();
}


// callback function
function callMe() {
   console.log('I am a callback function');
}


// passing the callback function as an argument
greet('John', callMe);
Enter fullscreen mode Exit fullscreen mode

Promises

Promises represent the eventual completion (or failure) of an asynchronous operation and its resulting value.

const promise = new Promise((resolve, reject) => {
   setTimeout(() => {
       resolve('Data received');
   }, 1000);
});


promise.then((data) => {
   console.log(data); // 'Data received'
}).catch((error) => {
   console.error(error);
});

Enter fullscreen mode Exit fullscreen mode

Async/Await

async and await provide a more readable and synchronous-like way to handle asynchronous operations using promises.

function fetchUser() {
   return new Promise((resolve) => {
     setTimeout(() => {
       resolve('User data fetched');
     }, 2000);
   });
 }
  // Async function to handle the asynchronous task
 async function getUserData() {
   console.log('Fetching user data...');
   const result = await fetchUser();
   console.log(result);
   // Expected output: "User data fetched"
 }


getUserData();
Enter fullscreen mode Exit fullscreen mode

The Event Loop

The event loop is the core mechanism that handles asynchronous operations in JavaScript. It continuously checks the task queue to see if any tasks need to be executed.

Task Queue

The task queue (also known as the callback queue) is where asynchronous tasks are placed once they are ready to be executed. This includes tasks such as timers (setTimeout, setInterval), I/O operations, and other callbacks.

function first() {
   console.log(1);
}


function second() {
   setTimeout(() => {
       console.log(2);
   }, 0);
}


function third() {
   console.log(3);
}
// Execute the functions
first();
second();
third();
Enter fullscreen mode Exit fullscreen mode

Output

Console output

This example demonstrates how setTimeout with a delay of 0 milliseconds does not execute immediately but instead, after the current stack of synchronous code has been executed. The event loop ensures that the asynchronous code (the timeout callback) is executed only after the call stack is empty.

Memory Storage

JavaScript automatically allocates memory when objects are created and frees it when they are not used anymore (garbage collection).

Garbage Collection

Garbage collection is the process of identifying and reclaiming memory that is no longer in use by the program. JavaScript engines, such as V8 (used in Chrome and Node.js)

Memory Life Cycle

The memory life cycle in JavaScript refers to the process by which memory is allocated, used, and then freed during the execution of a program. The memory life cycle consists of three phases.

  • Allocation: Memory is allocated when variables, objects, or data structures are created.

  • Usage: The allocated memory is used for operations like reading or writing to variables and objects.

  • Release: Memory is released when it is no longer needed. This is where garbage collection comes in.

Primitive and Reference Data Types

Primitive Data Types: Primitive data types are the most basic data types in JavaScript. They are immutable, meaning their values cannot be changed once created. stored by value directly in the stack. (Number, String, Boolean, Undefined, Null, Symbol)

Reference Data Types: Reference data types, also known as objects, are more complex data structures. Unlike primitives, they are mutable, stored by reference in the heap. (Object, Array, Function)

Stack and Heap

JavaScript, like many programming languages, uses two main memory structures to manage the allocation and storage of data: the stack and the heap.

Stack and Heap

The diagram illustrates how primitive values (like numbers and strings) and reference values (like objects) are handled in JavaScript. Primitive values are copied directly when assigned to variables. Reference values, on the other hand, store a memory address that points to the actual object in the heap. When you assign a reference value, you copy the memory address, not the object itself.

Javascript Engine

A JavaScript engine is a program or interpreter that executes JavaScript code. Each web browser has its own JavaScript engine to parse, compile, and execute JavaScript code.

Browser or Runtime JavaScript Engine
Mozilla Spidermonkey
Chrome, Node.js, Deno V8
Safari JavaScriptCore*
Edge Chakra
Bun JavaScriptCore

Compilation vs. Interpretation in Programming Languages

Compilation: Compilation is the process of translating high-level source code into machine code (binary code) that a computer's processor can execute directly. (C, C++, GO, Rust)

Interpretation: Interpretation involves translating and executing high-level source code line-by-line or statement-by-statement at runtime. (Python, Ruby, PHP, Javascript)

Just-In-Time (JIT) Compilation

Just-In-Time (JIT) compilation is a technique used by JavaScript engines to enhance performance by compiling frequently executed JavaScript code into native machine code at runtime. This approach allows for faster execution by leveraging both initial interpretation for quick start-up and ongoing optimization for code that is executed frequently, resulting in adaptive and efficient performance improvements.

JavaScript runtime environment

This illustration represents the JavaScript runtime environment, focusing on how the V8 engine, the event loop, and the browser environment interact to execute JavaScript code. When a developer runs a JavaScript script on the V8 engine, the engine performs the following steps:

  1. Compilation and Execution: The engine compiles and executes the JavaScript code using Just-In-Time (JIT) compilation for optimized performance.

  2. Call Stack Management: V8 handles the call stack, managing function calls and execution order to ensure proper code execution.

  3. Memory Management: The engine manages the memory heap, allocating and deallocating memory as needed for objects, arrays and functions.

  4. Garbage Collection: V8 performs garbage collection, automatically cleaning up memory that is no longer in use to prevent memory leaks and optimize performance.

  5. Data Types and Objects: The engine provides all the necessary data types, objects, and functions required for JavaScript execution, adhering to the ECMAScript standard.

  6. Event Loop: V8 also integrates the event loop (sometimes implemented by the browser), which handles asynchronous operations like callbacks, promises, and event handling to ensure non-blocking execution.

Conclusion

Understanding JavaScript's core concepts provides a solid foundation for developing efficient and high-performance applications. Threads and the call stack are crucial for managing function calls and execution order, operating on a Last In, First Out (LIFO) principle. Execution contexts, including the global and functional execution contexts, determine the scope and lifetime of variables and functions. Asynchronous operations, enabled by callbacks, promises, and async/await, allow JavaScript to handle time-consuming tasks without blocking the main thread. Memory management, through efficient allocation and garbage collection, ensures optimal performance. The JavaScript engine, such as V8, compiles and executes the code, manages the call stack, memory heap, and handles garbage collection, ensuring the smooth operation of JavaScript applications.

Top comments (0)