When your browser encounters JavaScript code nestled within your HTML, it hands the code off to a specialised interpreter called the JavaScript engine which converts the code into computer understandable langauge.
The JavaScript Engine
Every web browser comes equipped with a JavaScript engine, a specialized program responsible for interpreting and executing JS code. Some of the popular JavaScript engines include Chrome's V8, Firefox's SpiderMonkey, and Safari's JavaScriptCore.
How JavaScript engine works?
Parsing: The engine starts reading your JavaScript line by line, breaking it down into its core elements like functions, variables, and expressions. This analysis builds an Abstract Syntax Tree (AST), a tree-like data structure that captures the structure and relationships between elements in your code.
Code Generation: The AST is then used to generate bytecode. Bytecode is a lower-level, machine-independent format that's easier for the engine to optimize. And now this byte code is ready for execution.
Optimization: When executing the bytecodes, JS engine keeps monitoring the codes and start optimizing code parallelly by compiling it into machine code. This is called Just-in-Time compilation. This compiled machine code runs much faster than interpreted bytecode, giving JavaScript a performance boost.
But What if the optimization fails?
If the optimization fails for any reason, then the compiler de-optimizes codes and let the interpreter executes the original bytecodes.
The Execution Context : Deep Dive
When ever your browser encounters JavaScript code, the browser's JavaScript engine then creates a special environment to handle execution of this code. This environment is known as the Execution Context.
During the Execution Context run-time, the code gets parsed and, the variables and functions are stored in memory, executable byte-code gets generated, and the code gets executed.
Two types of execution context are there-
Global Execution Context (GEC) : When a JavaScript program initiates execution, the browser's JavaScript engine establishes the Global Execution Context (GEC). This GEC serves as the foundation upon which all other contexts are built.
Function Execution Context (FEC): Every time a function is invoked in JavaScript, a new Function Execution Context (FEC) is created. This FEC becomes the active context during the function's execution.
Since every function call gets its own FEC, there can be more than one FEC in the run-time of a script.
Creation of Execution Context
The creation of execution context occurs in three steps ->
- Creation of the Variable Object (VO): The VO is a container that stores variables and function declarations within the execution context. In the GEC, variables declared with var are added to the VO and initialised to undefined.
Also, function declaration are stored in memory. This means that all the function declarations will be stored and made accessible inside the VO, even before the code starts running.
FECs, on the other hand, don't have a VO. Instead, they create an argument object containing the arguments passed to the function.
This process of storing variables and functions before execution is called hoisting.
- Creation of the Scope Chain: Scope determines where variables and functions are accessible in your code.
Each FEC creates its own scope chain, which is a hierarchy of scopes. The function's local scope is at the top, followed by the scopes of its parent functions, all the way up to the GEC.
When a function is defined in another function, the inner function has access to the code defined in that of the outer function, and that of its parents. This behavior is called lexical scoping.
However, the outer function does not have access to the code within the inner function.
This concept of scope brings up an associate phenomenon in JavaScript called closures.
function outerFunction() {
var outerVar = "I'm from the outer function!";
function innerFunction() {
// Accessing outerVar from the inner function
console.log("Inner function can access outerVar:", outerVar);
var innerVar = "I'm from the inner function!";
console.log("Inner variable:", innerVar);
}
innerFunction();
}
outerFunction();
//Output
//Inner function can access outerVar: I'm from the outer function!
//Inner variable: I'm from the inner function!
In above example if we try to access inner var in outer function by console logging inner var, it will give error.
- Setting the Value of the this Keyword: The this keyword refers to the current execution context.
"this"in Global Context
In the GEC, this refers to the global object (usually the window object in browsers).
"this"in Functions
In the case of the FEC, it doesn't create the this object. Rather, it get's access to that of the environment it is defined in.
The Execution Phase
Once the creation phase is complete, the JavaScript engine moves on to the execution phase. Here, the Javascript engine reads code line by line once more in the current execution context, and the variable which are set to undefined in VO, assigned with actual values, functions are called, and the code gets transpired to executable byte code, and finally gets executed.
The Call Stack: Keeping Track of Function Calls
The Call Stack is a LIFO (Last-In-First-Out) data structure that manages function calls. As a function is invoked, a new Stack Frame is pushed onto the call stack. This frame holds information about the function's arguments, local variables, and the return address (where to continue after the function finishes). When the function returns, its stack frame is popped off the stack, and execution resumes at the return address.
var name = "John";
function func1() {
var a = "Hello";
func2();
console.log(`${a} ${name}`);
}
function func2() {
var b = "World";
console.log(`${b} ${name}`);
}
func1();
Initially as the js code loaded on the browser, the Global execution context is being created and pushed to the bottom of the Call Stack. The name variable is present inside the GEC.
Now, when js engine encounters func1 call, it creates Function execution context for func1 and this new context is placed on top of the current context, forming the Execution Stack.
The variable 'a' which is inside the func1 is stored in the Function execution context(FEC) not in GEC.
Similary FEC is created for func2 when encounters function call inside the func1.
As the functions execution is finished, the function execution context of the function is popped out one by one from the top and at last reaches the Gobal Execution Context remains in the stack.
And lastly, when the execution of the entire code gets completed, the JS engine removes the GEC from the current stack.
The Event Loop: Handling Asynchronous Operations
JavaScript is single-threaded, meaning it can only execute one task at a time. However, it can handle asynchronous operations, through the Event Loop.
Let us understand how it works:
Main Thread: Executes the main JS code sequentially.
Callback Queue: The callback queue includes callback functions that are ready to be executed. The callback queue ensures that callbacks are executed in the First-In-First-Out (FIFO) method and they get passed into the call stack when it’s empty.
Event Loop: Continuously monitors the main thread and the callback queue. When the main thread becomes idle (after executing a synchronous task or reaching the end of the code), the event loop dequeues a callback from the callback queue and pushes the callback onto the call stack for execution
This approach allows JS to appear responsive while handling asynchronous operations without blocking the main thread.
Top comments (0)