Understanding How JavaScript works – Execution Flow with example

JavaScript is a single-threaded, event-driven programming language designed for the development of dynamic and interactive web pages. Understanding the JavaScript execution flow is crucial for writing efficient and bug-free code. The JavaScript execution flow is the sequence of steps that a JavaScript engine follows to interpret and run JavaScript code. To understand this process, let’s break it down into several key components:

Source Code:

The JavaScript execution process begins with the source code, which is the actual JavaScript program written by the developer.

Lexical Analysis (Tokenization):

The source code is first passed through a process called lexical analysis or tokenization. During this phase, the code is broken down into individual tokens, which are the smallest units of meaningful code, such as keywords, identifiers, operators, and literals.

Abstract Syntax Tree (AST) Creation:

The tokens generated from lexical analysis are used to build an Abstract Syntax Tree (AST). The AST represents the syntactic structure of the code and is a hierarchical tree-like structure where each node corresponds to a syntactic construct in the code (e.g., statements, expressions).

Parsing:

The JavaScript engine parses the AST to understand the structure and meaning of the code. Parsing involves analyzing the relationships between different elements in the AST to create a structured representation of the code.

Execution Context:

Before code execution begins, an execution context is created. The execution context is a data structure that contains information about the code being executed. There are two types of execution contexts: global context and function context.

Variable and Function Hoisting:

Before the actual code execution, JavaScript performs hoisting. This means that variable and function declarations are moved to the top of their containing scope. However, only the declarations are hoisted, not the initializations.

Scope Chain and Lexical Scoping:

JavaScript uses lexical scoping, which means that the scope of a variable is determined by its location in the source code. The scope chain is established based on the nested structure of functions, allowing inner functions to access variables from outer functions.

Variable Instantiation:

Variables are assigned initial values in a process known as variable instantiation. This step occurs in the creation phase of the execution context.

Code Execution:

The actual code is executed line by line. During this phase, the JavaScript engine performs various operations, such as assigning values to variables, evaluating expressions, and executing statements.

Function Invocation and Call Stack:

The call stack is a data structure that keeps track of the currently executing functions. It follows the Last In, First Out (LIFO) principle

When a function is called, a new execution context is created for that function. The call stack keeps track of the currently executing contexts. After the function completes, its context is removed from the stack.

Ensures functions are executed in the order they are called. Manages the flow of synchronous code.

Asynchronous Operations and Event Loop:

JavaScript supports asynchronous operations, such as setTimeout, AJAX requests, and event listeners. The event loop is a mechanism that continuously checks the call stack and message queue for tasks to execute. It ensures non-blocking asynchronous operations. When the call stack is empty, it picks tasks from the callback queue and pushes them onto the call stack.

Callback Queue:

The callback queue (also known as the message queue) holds callback functions and events that are ready to be executed after the current execution stack is cleared.

Asynchronous tasks, like timers or I/O operations, place their callbacks in the callback queue. The event loop transfers tasks from the queue to the call stack when it’s empty.

Microtask Queue:

The microtask queue holds higher-priority tasks compared to the callback queue. Microtasks are usually associated with Promises.

Microtasks are executed after the current execution stack is cleared but before the next event loop cycle. Promises and certain APIs, like queueMicrotask, add tasks to the microtask queue.

Microtasks have higher priority than regular tasks in the event loop.

Memory Management and Garbage Collection:

JavaScript manages memory through automatic garbage collection. Objects that are no longer reachable are identified and released to free up memory.

Let’s create an example that covers various execution concepts in JavaScript. Consider a scenario where we have a simple asynchronous operation, closure, and lexical scoping:

// Lexical Scoping and Closure
function outerFunction(outerVar) {
  // outerVar is a variable in the outer function's scope

  function innerFunction(innerVar) {
    // innerVar is a variable in the inner function's scope
    console.log(`Inside innerFunction: ${outerVar} ${innerVar}`);
  }

  // Returning the inner function creates a closure
  return innerFunction;
}

const closureInstance = outerFunction("I'm outer!");

// Asynchronous Operation
console.log("Start");

setTimeout(function () {
  console.log("Timeout");
}, 0);

Promise.resolve().then(function () {
  console.log("Promise resolved");
});

console.log("End");

// Execution Context and Scope Chain
closureInstance("I'm inner!");

// Output:
// Start
// End
// Promise resolved
// Timeout
// Inside innerFunction: I'm outer! I'm inner!

In this example:

  1. Lexical Scoping and Closure:
    • outerFunction takes an argument outerVar and defines an innerFunction inside it.
    • innerFunction has access to both its own variable (innerVar) and the outer function’s variable (outerVar) due to closure.
    • The returned innerFunction forms a closure over the outerFunction‘s scope.
  2. Asynchronous Operation:
    • We use setTimeout to simulate an asynchronous task.
    • Additionally, a Promise with a then callback is used to demonstrate microtasks in the event loop.
    • The order of execution showcases the event loop’s behavior.
  3. Execution Context and Scope Chain:
    • closureInstance is a reference to the innerFunction created by outerFunction.
    • When closureInstance is invoked, it still has access to the variables in the scope of outerFunction.

Understanding this example helps illustrate how different aspects of JavaScript execution, such as lexical scoping, closures, asynchronous operations, and execution context, work together in a real-world scenario.