Home - NeverGiveUp143/JS GitHub Wiki
JS
JS Engine
A JavaScript engine is a software component that executes JavaScript code. It translates human-readable JavaScript code into machine-readable instructions that the computer's hardware can execute.
Notable JavaScript Engines
-
Javascript source code is passed to "Parser"
-
Parser divides the code into multiple tokens
-
It is converted into AST (Abstract Syntax Tree), a tree-like structure that represents functions, conditionals, scopes etc.
-
This AST is passed to the interpreter which converts the code into Bytecode.
-
At the same time engine is actually running the Javascript code
-
Bytecode is used by optimizing compiler along with profiling data
-
"Optimizing compiler" make certain assumptions based on profiling data and produces highly optimized machine code.
Sometimes there is a case where the 'optimization' assumption is incorrect and then it goes back to the previous version via the "Deoptimize" phase (where it actually becomes the overhead to us)
JS Engine usually optimizes "hot functions" and uses inline caching techniques to optimize the code.
Execution Flow of V8
The execution flow in the V8 JavaScript engine can be summarized as follows:
Parsing and Compilation
Parsing
- The V8 engine first parses the JavaScript code into an Abstract Syntax Tree (AST).
- The parser breaks down the code into tokens and constructs the AST, which represents the hierarchical structure of the code.
Compilation
- V8 uses a Just-In-Time (JIT) compilation approach to convert the AST into optimized machine code.
- Initially, the Ignition interpreter compiles the AST into bytecode, which is a lower-level representation of the JavaScript code.
- The TurboFan compiler then analyzes the bytecode and generates highly optimized machine code that can be efficiently executed.
Execution
Call Stack and Heap
- V8 maintains a call stack to keep track of the functions being called and their execution contexts.
- The heap is where objects and other data structures are stored in memory.
Event Loop
- Since V8 lacks its own event loop, it integrates with the event loop provided by the hosting environment (e.g., the browser's event loop or the libuv library in Node.js).
- The event loop handles asynchronous tasks, such as I/O operations, by offloading them to separate threads or the operating system, ensuring the main JavaScript thread remains responsive.
Inline Caching
- V8 uses Inline Caching (IC) to optimize property access and method calls.
- ICs keep track of the shapes of objects and the locations of their properties, allowing for faster lookups during execution.
Garbage Collection
- V8 employs a generational garbage collector to automatically manage the memory used by the JavaScript application.
- The garbage collector periodically identifies and reclaims memory occupied by objects that are no longer in use.
By leveraging these techniques, the V8 JavaScript engine is able to provide high-performance execution of JavaScript code, enabling the development of complex and responsive web applications and server-side JavaScript environments like Node.js.
JavaScript Runtime
A JavaScript runtime environment is a software framework that allows JavaScript code to be executed outside of a web browser. The most popular JavaScript runtime environments is:
Node.js
Node.js is a free, open-source, cross-platform JavaScript runtime environment that enables developers to run JavaScript on the server-side. It was created in 2009 and allows developers to create servers, web apps, command line tools and scripts using JavaScript. Node.js is built on top of the V8 JavaScript engine, which is the same engine used by the Google Chrome browser to execute JavaScript. This allows Node.js to provide a server-side runtime for executing JavaScript code
Interpreter , Compiler and JIT
An interpreter, compiler, and JIT (Just-In-Time) compiler are different approaches for executing code written in a high-level programming language:
Interpreter
An interpreter directly executes the source code or bytecode line by line without compiling it to machine code first. It translates each statement into machine instructions and immediately executes them. Interpreted languages include Python, Ruby, and JavaScript (in web browsers).
Compiler
A compiler reads the entire source code, analyzes it, and translates it into machine code or executable program all at once before execution. The generated machine code can then be executed directly by the computer's processor. Compiled languages include C, C++, and Rust.
JIT Compiler
A JIT compiler is a combination of an interpreter and a compiler. It first interprets the bytecode, but when it identifies frequently executed code segments (hot spots), it compiles those segments into machine code at runtime. This allows the JVM to optimize the code based on runtime information. The compiled code is then cached for subsequent executions. JIT compilers are used in Java and .NET runtimes. They provide the flexibility of interpretation with the performance benefits of compilation for frequently executed code.
In summary:
Interpreter: Executes code line by line without compiling Compiler: Translates entire source code to machine code before execution JIT Compiler: Interprets bytecode initially, compiling frequently executed parts to optimized machine code at runtime
Call Stack And Memory Heap
The call stack and memory heap are two fundamental concepts in computer programming that manage how data is stored and accessed during the execution of a program. Understanding these concepts is crucial for optimizing performance and debugging issues such as memory leaks and stack overflows.
Call Stack
The call stack is a stack data structure that keeps track of active subroutines (or function calls) in a program. It follows the Last-In, First-Out (LIFO) principle, meaning the last function called is the first one to complete execution and return control to the previous function.
How It Works
When a function is called, a new stack frame is created and pushed onto the call stack. This stack frame contains:
Return Address: The location in the code to return to after the function finishes executing.
Local Variables: Variables defined within the function.
Parameters: Arguments passed to the function.
When the function completes, its stack frame is popped off the stack, and control returns to the function that called it. Example Consider the following JavaScript code:
function greet() {
console.log("Hello");
}
function main() {
greet();
}
main();
- When main() is called, a stack frame for main is created and pushed onto the call stack.
- Inside main(), greet() is called, so a new stack frame for greet is pushed on top of the stack.
- The greet function executes and prints "Hello", then its stack frame is popped off, returning control to main.
- Finally, main completes, and its stack frame is also popped off.
The call stack during execution would look like this:
| greet() |
| main() |
Stack Overflow
A stack overflow occurs when there are too many nested function calls, exhausting the stack's allocated memory. This is often due to excessive recursion without a base case. For example:
function recursive() {
recursive(); // Calls itself indefinitely
}
recursive();
This will eventually lead to a stack overflow error.
Memory Heap
Definition
The memory heap is a region of a computer's memory used for dynamic memory allocation. Unlike the call stack, the heap does not follow a strict LIFO order and allows for the allocation and deallocation of memory at any time.
How It Works
When a program needs to allocate memory for objects or data structures that may change in size or need to persist beyond the current function call, it uses the heap. Memory on the heap is managed through pointers and references, allowing for more flexible memory usage. Example In JavaScript, objects and arrays are stored in the heap:
let obj = { name: "Alice" }; // Allocated in the heap
let arr = [1, 2, 3]; // Allocated in the heap
In this case, obj and arr are stored in the heap, and their references are stored in the call stack. When a function that uses these variables completes, the references in the call stack are removed, but the objects in the heap remain until they are no longer referenced and can be garbage collected.
Memory Management
Memory management in the heap can lead to issues such as memory leaks if allocated memory is not properly released. Languages like JavaScript manage memory automatically through garbage collection, which periodically frees up memory that is no longer in use.
Summary
Call Stack: A LIFO structure that manages function calls, storing return addresses, local variables, and parameters. It is prone to overflow with excessive recursion.
Memory Heap: A dynamic memory area that allows for flexible allocation and deallocation of memory for objects and data structures. It requires careful management to avoid memory leaks.
Understanding these two memory management concepts is crucial for efficient programming and debugging in languages that utilize them, such as JavaScript, C, and others.
Memory leak
A memory leak occurs when a program allocates memory on the heap but fails to release it after it is no longer needed. This can lead to increased memory usage over time, potentially exhausting available memory and causing the application or system to slow down or crash. Causes of Memory Leaks
Failure to Free Memory: When dynamically allocated memory is not freed.
Lost References: When pointers to allocated memory are overwritten or go out of scope, making it impossible to free the memory.
Circular References: In languages with garbage collection, circular references can prevent memory from being freed.Here are some common causes of memory leaks in JavaScript with examples:
- Failing to Clear Event Listeners Event listeners attached to DOM elements can lead to memory leaks if they are not properly removed when the associated DOM elements are removed or no longer needed.
// Attach event listener
document.getElementById('myButton').addEventListener('click', onClick);
function onClick() {
// Handle click event
}
// Remove button from DOM
document.getElementById('myButton').remove();
In this example, even though the button has been removed from the DOM, the onClick function is still referenced by the event listener, preventing it from being garbage collected. To fix this, you should remove the event listener when the button is removed:
// Attach event listener
const button = document.getElementById('myButton');
button.addEventListener('click', onClick);
function onClick() {
// Handle click event
}
// Remove button from DOM and clear event listener
button.removeEventListener('click', onClick);
button.remove();
2. Keeping References to Objects Unnecessarily Holding onto references to objects that are no longer needed can prevent them from being garbage collected, leading to memory leaks.
let cache = [];
function addToCache(obj) {
cache.push(obj);
}
function useCache() {
for (let i = 0; i < cache.length; i++) {
// Use cached object
}
}
In this case, if addToCache is called repeatedly without removing items from the cache array, the memory used by the cached objects will not be freed, causing a memory leak. To prevent this, you should remove references to objects when they are no longer needed:
let cache = [];
function addToCache(obj) {
cache.push(obj);
}
function useCache() {
for (let i = 0; i < cache.length; i++) {
// Use cached object
}
cache = []; // Clear cache
}
3. Circular References Circular references occur when two or more objects hold references to each other, preventing the garbage collector from collecting them. This can happen when using closures or certain data structures.
function createCircularReference() {
let obj1 = {};
let obj2 = {
ref: obj1
};
obj1.ref = obj2;
return obj1;
}
let circularRef = createCircularReference();
In this example, obj1 and obj2 have circular references, preventing them from being garbage collected. To avoid this, you should break the circular reference by setting one of the references to null:
function createCircularReference() {
let obj1 = {};
let obj2 = {
ref: obj1
};
obj1.ref = obj2;
return obj1;
}
let circularRef = createCircularReference();
circularRef.ref.ref = null; // Break circular reference
To avoid memory leaks in JavaScript, it's important to:
- Remove event listeners when associated DOM elements are removed or no longer needed.
- Remove references to objects when they are no longer needed.
- Break circular references by carefully managing object references.
Additionally, tools like the Chrome DevTools Memory panel can help identify and diagnose memory leaks in JavaScript applications.
Garbage collection
Garbage collection in JavaScript is the process of automatically freeing up memory occupied by objects that are no longer in use by the program. The JavaScript engine's garbage collector periodically identifies and removes these unused objects to prevent memory leaks and optimize memory usage.
Single Threaded Model
In JavaScript, the single-threaded model means that there is one call stack and one memory heap. The call stack is responsible for managing function execution, while the memory heap is used for storing objects and variables. When a function is called, it is pushed onto the call stack, and when it completes, it is popped off the stack.
Key Characteristics
- Synchronous Execution: JavaScript executes code in a sequential manner. If a function takes a long time to execute, it can block the execution of subsequent code until it completes.
2.** Event Loop:** To handle asynchronous operations without blocking the main thread, JavaScript uses an event loop. This allows the language to perform non-blocking I/O operations, such as fetching data from a server, while still being single-threaded.
- Asynchronous Programming: JavaScript employs callbacks, promises, and async/await to manage asynchronous tasks, allowing other code to run while waiting for a long-running operation to complete.
Example of the Single-Threaded Model
Consider the following code snippet that demonstrates how JavaScript handles synchronous and asynchronous operations:
console.log("Start");
setTimeout(() => {
console.log("Timeout finished");
}, 2000);
console.log("End");
Output Explanation When this code is executed, the output will be:
Start
End
Timeout finished
Breakdown of Execution
1.** console.log("Start"):** This line is executed first and prints "Start" to the console. It is placed on the call stack and then removed after execution.
2.** setTimeout(...): **This function is called next. It sets a timer for 2000 milliseconds (2 seconds) but does not block the execution of the next line. Instead, it registers the callback to be executed after the timer completes and immediately returns control back to the main thread.
3.** console.log("End"):** This line is executed next, printing "End" to the console.
4.** Timeout Callback:** After 2 seconds, the event loop checks the timer and sees that the callback function from setTimeout is ready to be executed. It is then pushed onto the call stack, and "Timeout finished" is printed.
Execution Context
In JavaScript, the execution context is a crucial concept that defines the environment in which JavaScript code is evaluated and executed. It encompasses the variable scope, the value of this, and the call stack. Understanding execution contexts is essential for grasping how JavaScript manages function calls, variable access, and scope.
Types of Execution Contexts
There are three primary types of execution contexts in JavaScript:
Global Execution Context: This is the default context in which any JavaScript code runs. It is created when the JavaScript engine starts executing the script. In the global context, variables and functions are accessible from anywhere in the code.
Function Execution Context: Each time a function is invoked, a new execution context is created for that function. This context contains the function's local variables, parameters, and the value of this. When the function completes, its execution context is removed from the call stack.
Eval Execution Context: This context is created when code is executed inside the eval() function. It is not commonly used and is generally discouraged due to security and performance concerns.
Execution Context Stack
JavaScript uses a call stack to manage execution contexts. The call stack is a stack data structure that keeps track of function calls. When a function is called, its execution context is pushed onto the stack. When the function returns, its context is popped off the stack. Example of Execution Context
Consider the following example to illustrate execution contexts:
let globalVar = 'I am a global variable';
function outerFunction() {
let outerVar = 'I am from outer function';
function innerFunction() {
let innerVar = 'I am from inner function';
console.log(globalVar); // Accessing global variable
console.log(outerVar); // Accessing outer function variable
console.log(innerVar); // Accessing inner function variable
}
innerFunction();
}
outerFunction();
Breakdown of Execution Contexts
Global Execution Context: When the script starts, the global context is created, and globalVar is defined.
Calling outerFunction(): The execution context for outerFunction is created and pushed onto the stack. Inside this context, outerVar is defined.
Calling innerFunction(): The execution context for innerFunction is created and pushed onto the stack. Inside this context, innerVar is defined.
Accessing Variables: innerFunction can access innerVar, outerVar, and globalVar due to JavaScript's lexical scoping rules.
Returning from Functions: When innerFunction completes, its context is popped off the stack, and control returns to outerFunction. When outerFunction completes, its context is also removed from the stack, returning control to the global context.