JavaScript, the popular coding language for websites, might seem like magic when you see it in action. But, if you look closer, you'll find interesting steps and tools that make it work. In this blog, we're going to explore important parts of JavaScript like the event loop, execution context, and how it handles memory. We'll help you understand what's happening behind the scenes when you use JavaScript, so you can improve your coding skills.
Every web browser and JavaScript runtime has something called a JavaScript engine at its heart. This engine, which is a complex software, translates and runs your code. One of the best-known JavaScript engines is V8 (JavaScript engine), which is open-source and made by Google. It's known for being fast and efficient. You can find it inside popular browsers like Google Chrome and tools like Node.js, where it helps to run JavaScript code.
Before we start explaining how engine works behind the scene let us first explain Compilation vs Interpretation.
Compilation: Source code is translated into machine code or bytecode before execution. Optimized executable files are generated, resulting in faster execution. However, it may have limited portability and longer development cycles. Interpretation: Source code is executed line-by-line or statement-by-statement at runtime. More portable, simpler debugging, but slower performance due to the interpretation overhead. Just-In-Time (JIT) Compilation: Hybrid approach that combines elements of both compilation and interpretation. Translates bytecode into machine code at runtime, offering a balance of performance and portability, with a potential initial startup overhead. Modern JavaScript uses this approach.
In case of JavaScript it work like this:
When code is compiled it can be executed in JS Engine.
In JavaScript, an execution context is an abstract concept that describes the environment in which code is executed.
Each time a function is called, a new execution context is created for that function. Additionally, a global execution context is created when the script is first executed, representing the top-level scope of the code. When a function is invoked, a new execution context is pushed onto the call stack, and when the function returns, its execution context is popped off the stack.
Example of execution context:
And result will be:
Starting program
Entering foo
Entering bar
Entering baz
Leaving baz
Leaving bar
Leaving foo
Program finished
Here's what happens step by step:
console.log('Starting program')
is executed first.foo()
function is called, and it's added to the call stack.foo()
, the bar()
function is called, and it's added to the call stack on top of foo()
.bar()
, the baz()
function is called, and it's added to the call stack on top of bar()
.baz()
, there are no more function calls, so baz()
executes and is removed from the call stack.baz()
finished, the control goes back to bar()
, which then executes the remaining code and is removed from the call stack.bar()
finished, the control goes back to foo()
, which then executes the remaining code and is removed from the call stack.foo()
finished executing, and the control goes back to the global context, where the console.log('Program finished')
is executed.The call stack keeps track of the active functions and their execution contexts. When a function is called, its context is pushed onto the stack, and when it returns, its context is popped off the stack. This process continues until all the functions have completed their execution and the stack becomes empty.
It follows the Last-In-First-Out (LIFO) principle, meaning that the most recently called function is the first to be executed and completed before moving on to the next one.
The three main components of an execution context are:
Variable Environment: Every time a function is called or a block of code is executed, a new execution context is created. Each execution context has its own Variable Environment, which represents the local scope of that particular function or block of code. This ensures that variables declared inside a function or block do not interfere with variables in other parts of the code.
When a variable is accessed or a function is called, JavaScript searches for it first in the current Variable Environment's Record. If the variable or function is not found there, it will look into the Outer Environment Reference and continues searching in the parent scope, creating a chain of Variable Environments known as the "scope chain."
Scope chain or Lexical Environment: The scope determines where these identifiers (variables, functions, etc.) are accessible and where they are not. JavaScript has two main types of scope:
Global Scope: Variables declared outside any function or block have global scope. They are accessible from anywhere in the code, including other functions and blocks.
Local Scope: Variables declared within a function or block have local scope. They are accessible only within the function or block in which they are declared.
Block Scope: Block scope refers to the visibility and accessibility of variables and functions within a specific block of code, typically delimited by curly braces {}. Before the introduction of ES6 (ECMAScript 2015), JavaScript only had function-level scope, but ES6 introduced the let and const keywords, which allow variables to have block-level scope.
When a variable is referenced in a JavaScript program, the JavaScript engine looks for that variable's value in the current scope. If it doesn't find it there, it moves up the scope chain until it finds the variable or reaches the global scope. This process of searching for a variable in the scope chain is known as the "Scope Chain.
Hoisting is explained in this blog
This Binding: In JavaScript, the this
keyword is a special variable that refers to the context of the current execution. Its value depends on how and where it is used. Understanding how this behaves is essential for writing effective and maintainable JavaScript code. The value of this
can be determined by the way a function is called:
this
is used outside of any function or object, it refers to the global object
, which is typically window
in browsers or global
in Node.js.this
is used within a regular function (not an arrow function), its value depends on how the function is called:this
will point to the global object
(in non-strict mode) or undefined
(in strict mode).
this
will refer to the object that owns the method.
do not have their own this context
. Instead, they inherit the value of this
from the surrounding lexical scope
.
new
keyword, this
refers to the newly created instance.
(call, apply, and bind)
to explicitly set the value of this
when calling a function.
Creation Phase: In this phase, the JavaScript engine first creates a variable object (also known as Activation Object) that contains all the variables, function declarations, and arguments defined inside the context. In the creation phase, variables are initialized with a default value of undefined
, while functions are already fully defined. The scope chain (the hierarchy of parent scopes in nested functions) is also determined in this phase. The this value is determined in the creation phase as well, based on the calling context of the function.
Execution Phase: In this phase, the JavaScript engine starts executing the code line by line. Here, the values of variables are updated with their actual values as they are encountered in the code.
Before we explain memory heap we need to understand that in JavaScript, there is a fundamental distinction between primitive data types and objects when it comes to how they are stored in memory. Understanding this difference is crucial for efficiently managing memory and optimizing code performance.
Primitives: JavaScript has six primitive data types: undefined, null, boolean, number, string, and symbol. When you create a variable to hold a primitive value, the value itself is directly stored in the variable. Primitives are immutable, which means their values cannot be changed after they are created. Each primitive value is assigned a fixed amount of memory based on its data type. If you assign a new value to a variable that previously held a primitive, the old value is discarded, and the new value is stored in its place.
Reference types: Everything except the primitives are Objects in JavaScript, including objects, arrays and functions, and they are called reference data types in JavaScript. When you create an object, the variable stores a reference to the memory location where the object is stored, rather than the object's value itself. Objects are mutable, and their values can be modified after creation.
In this case, person contains a reference to the memory location where the object { name: "Alice", age: 30 } is stored. When assigning an object to a new variable or passing it as an argument to a function, the reference is copied, not the object itself. As a result, multiple variables can refer to the same object in memory.
So Memory heap is a region in the computer's memory used by the JavaScript engine to allocate memory dynamically during the runtime of a program. It is where objects are stored.
Key characteristics of the JavaScript memory heap:
Dynamic Allocation: Unlike some low-level programming languages where memory must be manually allocated and deallocated, JavaScript's memory heap automatically manages memory allocation. When objects or data structures are created, JavaScript's memory manager finds a suitable space in the heap to store them.
Garbage Collection: JavaScript uses a garbage collector to automatically deallocate memory that is no longer needed. The garbage collector periodically scans the memory heap for objects that are no longer accessible or referenced by the program. When an object is no longer reachable, the garbage collector frees up the memory it was occupying, making it available for future allocations.
Object Allocation: Objects in JavaScript, including arrays and functions, are stored in the memory heap. When you create an object, either through object literals or constructor functions, memory is allocated in the heap to store the object and its properties.
Reference Counting: JavaScript's garbage collector primarily uses a technique called "reference counting" to determine which objects are no longer in use. Each object in memory has a reference count that keeps track of the number of references to it. When the reference count of an object drops to zero, meaning no variable or data structure refers to it, the garbage collector identifies it as garbage and reclaims its memory.
Memory Leaks: Although JavaScript has automatic garbage collection, memory leaks can still occur if objects are unintentionally kept in memory despite no longer being needed. This can happen when circular references are present or when developers are not careful with their code, leading to retained references to objects that should have been released.
Asynchronous JavaScript will be explained in a separate blog post
Share:
Accelerating Digital Success. Experience the future of web development – faster, smarter, better. Lets innovate together.
©2024 Dreit Technologies | All rights reserved