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.
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.
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.
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.
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.
In JavaScript, the call stack uses the LIFO principle to manage function calls:
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.
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:
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
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 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 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.
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);
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);
});
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();
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.
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();
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.
JavaScript automatically allocates memory when objects are created and frees it when they are not used anymore (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)
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.
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).
JavaScript, like many programming languages, uses two main memory structures to manage the allocation and storage of data: the stack and the 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.
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.
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 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.
External source: https://cabulous.medium.com/how-v8-javascript-engine-works-5393832d80a7
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:
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.