JavaScript Unwrapped (Part VI) - Understand how JS works BTS!
Topic: Understand how JS works Behind The Scenes(BTS)
Introduction
Let us understand some keywords that can be used to describe JS as a language. I have included real-world examples to describe each concept for better understanding of the terms that we are going to look at.
1. High-Level Language
Real-World Example: Imagine you're driving a car with an automatic transmission. You don't need to manually change gears or control the clutch; the car handles these details for you, making driving easier. However, if you had more control (like with a manual transmission), you could drive more efficiently, but it would be more complex.
Explanation: JavaScript is a high-level language, meaning it handles many low-level details for you, like memory management, making it easier to use. However, this abstraction can sometimes result in less optimized performance compared to low-level languages like C.
2. Garbage Collection
Real-World Example: Think of a robot cleaning your house. It automatically vacuums up dirt and debris without you needing to manually sweep the floor, keeping your home clean and tidy.
Explanation: In JavaScript, garbage collection is like that robot. The JavaScript engine automatically removes unused data (like variables and objects you no longer need) from memory, freeing you from having to clean it up manually.
3. Interpreted or Just-in-Time (JIT) Compiled
Real-World Example: Suppose you’re reading a book in a language you don’t know. You could either translate each word as you read (interpreted) or translate the entire book beforehand (compiled). If you translate each page as you go, you get a mix of both (JIT).
Explanation: JavaScript is often interpreted, meaning it's translated to machine code on the fly as it runs. However, modern JavaScript engines also use JIT compilation, where some parts of the code are compiled just before execution to improve performance.
4. Multi-Paradigm Language
Real-World Example: Consider a Swiss Army knife. It’s a single tool but can function as a knife, scissors, screwdriver, etc. You choose the tool based on your needs.
Explanation: JavaScript is multi-paradigm, supporting different programming styles like procedural, object-oriented, and functional programming. This flexibility allows you to pick the approach that best suits your project.
5. Object-Oriented Programming (Prototype-Based)
Real-World Example: Imagine a bakery where all cakes are made from the same recipe (a prototype). Each cake (object) has similar ingredients and steps, but you can add different decorations or flavors to individual cakes.
Explanation: In JavaScript, objects are created from prototypes, which serve as templates. For example, all arrays inherit methods like
push
from theArray
prototype, allowing us to use these methods on any array. The prototypal inheritance will be covered in detail in the OOP related articles!
6. First-Class Functions
Real-World Example: Think of functions like Lego blocks. You can connect them in various ways, pass them around, or even return one block from another to build something new.
Explanation: In JavaScript, functions are first-class citizens, meaning they can be stored in variables, passed as arguments, or returned from other functions. This capability makes functions incredibly flexible and powerful.
7. Dynamic Language
Real-World Example: Imagine a shape-shifting robot that can change its form depending on the situation. It could be a car one moment and a plane the next.
Explanation: JavaScript is dynamically typed, meaning variables can change types at runtime. For example, a variable can hold a number initially but later store a string. This flexibility can lead to powerful but sometimes unpredictable code.
8. Single-Threaded and Non-Blocking Event Loop
Real-World Example: Picture a chef in a kitchen with only one stove. The chef can only cook one dish at a time, but they can prepare ingredients while waiting for a dish to cook. The chef checks on the dish periodically and finishes other tasks in between.
Explanation: JavaScript runs on a single thread, meaning it can only do one thing at a time. However, it uses an event loop to handle multiple tasks by offloading long-running tasks (like fetching data, reading/writing a file) and revisiting them once they’re ready, ensuring the main thread doesn’t get blocked.
9. Concurrency Model
Real-World Example: Consider a secretary who manages a boss’s schedule. The secretary can handle many tasks (like answering emails, scheduling meetings) without waiting for the boss to finish a meeting. Once the boss is free, the secretary updates them on pending tasks.
Explanation: JavaScript’s concurrency model allows it to handle multiple tasks without blocking the main thread. The event loop ensures that tasks like user input or server requests are managed smoothly, allowing the code to stay responsive.
JavaScript Engine and Runtime: Detailed Explanation
To fully grasp how JavaScript works under the hood, it's crucial to understand the concepts of the JavaScript Engine and JavaScript Runtime. Both play a key role in executing JavaScript code, but they have distinct responsibilities. Let’s break them down in detail.
1. JavaScript Engine
A JavaScript Engine is the component that reads, interprets, and executes JavaScript code. All major browsers, like Chrome, Firefox, and Safari, come with their own JavaScript engine. For example:
Google Chrome uses the V8 engine.
Mozilla Firefox uses the SpiderMonkey engine.
Safari uses the JavaScriptCore engine.
Key Components of a JavaScript Engine
Memory Heap:
What it is: A region in memory where objects, functions, and variables are stored. Think of it as a place where data that your program works with resides.
Real-World Analogy: Imagine a large warehouse where different items are stored. Each item is carefully placed so it can be retrieved and used whenever needed.
Call Stack:
What it is: A stack data structure that keeps track of the function calls in your program. When a function is called, it is added (or "pushed") to the stack. When the function finishes execution, it is removed (or "popped") from the stack.
Real-World Analogy: Think of it as a stack of plates. When you add a new plate (function call), it goes on top. When you remove a plate, you always remove the top one first.
Execution Process:
The JavaScript engine executes code in two main phases:
Parsing: The code is parsed into an Abstract Syntax Tree (AST), which is a structured representation of the code.
Compilation: The engine either interprets the code line-by-line or compiles it into machine code, depending on whether it's an interpreted or JIT-compiled engine.
How It Works:
When you run JavaScript code, the engine parses the code and converts it into an intermediate representation called an Abstract Syntax Tree (AST).
The engine then either interprets this AST or compiles it into optimized machine code, which is then executed on the CPU.
2. JavaScript Runtime
The JavaScript Runtime provides the environment in which JavaScript code is executed. It includes everything necessary for the code to run outside the engine, such as the Web APIs, the event loop, and the callback queue.
Key Components of a JavaScript Runtime
JavaScript Engine:
The runtime includes the engine, which does the parsing and execution of code.
Web APIs (in browsers):
What they are: APIs provided by the browser, such as
DOM
,setTimeout
,fetch
, and more. These allow JavaScript to interact with the browser environment.Real-World Analogy: Imagine a library of tools or utilities that a craftsman uses. Each tool (API) has a specific purpose, like measuring, cutting, or fastening.
Callback Queue:
What it is: A queue where asynchronous operations like
setTimeout
orfetch
are placed once they are ready to be executed.Real-World Analogy: Picture a waiting line at a service desk. As each customer (callback) finishes their initial paperwork, they wait in line until the clerk (event loop) calls them to the desk for processing.
Event Loop:
What it is: A loop that continuously checks if the call stack is empty and if there are any tasks in the callback queue waiting to be processed. If the call stack is empty, the event loop pushes the first task from the callback queue onto the call stack for execution.
Real-World Analogy: Imagine a receptionist at an office. When a customer arrives (an event or callback), the receptionist (event loop) checks if the boss (call stack) is free. If they are, the customer is sent in (processed); otherwise, they wait until the boss is available.
How It Works:
When you write code that includes asynchronous operations (like
setTimeout
or a network request), these tasks are delegated to the Web APIs. Once they complete, their associated callback functions are moved to the callback queue.The event loop then checks if the call stack is clear. If it is, the next task from the callback queue is executed.
This ensures that JavaScript remains non-blocking and can handle multiple tasks, even though it's single-threaded.
Putting It All Together
When you write JavaScript code, here’s what happens:
Execution starts with the JavaScript engine processing your synchronous code, pushing each function call onto the call stack and executing it.
If an asynchronous operation is encountered (e.g.,
setTimeout
), the operation is passed to the Web APIs (provided by the browser) to handle.Once the asynchronous operation completes, its callback is sent to the callback queue.
The event loop continuously checks the call stack. If it’s empty, the event loop takes the first task from the callback queue and pushes it to the call stack for execution.
Real-World Example of How JavaScript Runtime Works
Consider a simple code snippet:
console.log('Start');
setTimeout(() => {
console.log('Async operation done');
}, 2000);
console.log('End');
Step-by-Step Execution:
console.log('Start')
is pushed to the call stack and executed, printing "Start."The
setTimeout
function is pushed to the call stack. It’s recognized as an asynchronous operation, so the browser’s Web API handles the timer. The callback (console.log('Async operation done')
) is registered to be executed after 2 seconds, andsetTimeout
is popped from the call stack.console.log('End')
is pushed to the call stack and executed, printing "End."After 2 seconds, the timer completes, and the callback function is pushed to the callback queue.
The event loop checks if the call stack is empty. Since it is, the callback function is moved from the callback queue to the call stack and executed, printing "Async operation done."
The JavaScript Engine is responsible for reading and executing your JavaScript code.
The JavaScript Runtime provides the environment, including the engine, Web APIs, the callback queue, and the event loop, to run and manage asynchronous operations.
Understanding these components and how they work together gives you a deeper insight into how JavaScript handles tasks, both synchronous and asynchronous, making it a powerful yet complex language.
What is the difference between Execution Context and Call Stack in JS?
In JavaScript, understanding how the code is executed is crucial for mastering the language. Two key concepts involved in this process are Execution Context and the Call Stack. Let’s break them down.
1. Execution Context
An Execution Context is an environment in which JavaScript code is evaluated and executed. It contains everything that is necessary to execute a piece of code.
Types of Execution Contexts
Global Execution Context:
What it is: Created when your script first runs. It’s the default execution context, where all your global code (code not inside any function) is executed.
What it contains:
Global Object: In the browser, this is the
window
object; in Node.js, it’s theglobal
object.this
keyword: In the global context,this
refers to the global object.Variable Environment: Stores global variables and function declarations.
Function Execution Context:
What it is: Created whenever a function is invoked. Each function call gets its own execution context.
Take a pause and try to recollect the Function Context that we had done in this article on Function in JS
What it contains:
Arguments Object: An array-like object that holds all the arguments passed to the function.
Variable Environment: Stores variables defined within the function.
this
keyword: Depends on how the function is called (can refer to different objects).
Eval Execution Context:
What it is: Created when code is executed inside an
eval()
function.Note: Generally, avoid using
eval()
as it can lead to security risks and performance issues.
Phases of Execution Context
Each execution context goes through two phases:
Creation Phase:
What happens:
Global context: The global object and
this
are created.Function context: Local variables, function declarations, and the
arguments
object are created.Hoisting: Variables and function declarations are “hoisted” (moved to the top of their scope) but not yet assigned values.
Execution Phase:
What happens:
The code inside the execution context is executed line by line.
Variables are assigned their values, and functions are invoked.
2. Call Stack
The Call Stack is a mechanism that tracks the order of function execution in JavaScript. It's a stack data structure that manages function calls and controls the execution context.
How the Call Stack Works
LIFO (Last In, First Out): The call stack operates on a LIFO principle, meaning the last function pushed onto the stack is the first one popped off.
Function Call: When a function is invoked, a new execution context is created and pushed onto the call stack.
Function Return: When the function finishes execution, its execution context is popped from the stack.
Global Context: The global execution context remains at the bottom of the stack until the program finishes.
Example: Call Stack in Action
(Even if you get a rough idea of the JS Engine and Runtime, it is enough at this point. This concepts will become crystal clear when we create Asynchronous codes in JS)
function first() {
console.log('First');
second();
console.log('First again');
}
function second() {
console.log('Second');
third();
}
function third() {
console.log('Third');
}
first();
Step-by-Step Execution:
Global Execution Context:
Created when the script starts. It’s pushed onto the call stack.
first()
is called:A new execution context for
first()
is created and pushed onto the call stack.
console.log('First')
is executed:Prints "First".
Execution moves to the next line in the
first()
function.
second()
is called:A new execution context for
second()
is created and pushed onto the call stack.
console.log('Second')
is executed:Prints "Second".
Execution moves to the next line in the
second()
function.
third()
is called:A new execution context for
third()
is created and pushed onto the call stack.
console.log('Third')
is executed:Prints "Third".
third()
finishes execution, and its context is popped off the call stack.
second()
finishes:The
second()
context is popped off the stack.
Execution returns to
first()
:The line
console.log('First again')
is executed, printing "First again".first()
finishes execution, and its context is popped off the stack.
Global Context remains:
The script finishes, and the global context is popped off the stack.
Real-World Analogy
Think of the call stack as a stack of books. Each time you open a book (call a function), you place it on top of the stack. When you finish reading it (the function returns), you remove it from the top of the stack. The process continues until there are no more books (functions) left, and you finally close the main cover (global context).
Execution Context is the environment where your JavaScript code is executed, including the variables, objects, and functions available at any point in time.
Call Stack is a data structure that keeps track of all the execution contexts, allowing JavaScript to manage function execution in a controlled, orderly manner.
Understanding Scope and Scope Chain in JavaScript
In JavaScript, scope and the scope chain are fundamental concepts that determine how variables and functions are accessed in your code. Understanding them is crucial for writing efficient, bug-free programs.
What is Scope?
Scope in JavaScript refers to the current context of execution in which variables, functions, and objects are accessible. Scope determines the visibility and accessibility of these variables within the code.
Types of Scope
Global Scope:
What it is: Variables declared outside of any function or block are in the global scope.
Characteristics:
Accessible from anywhere in your code.
In a browser, variables in the global scope are attached to the
window
object.
Example:
Function Scope:
What it is: Variables declared inside a function are in the function scope (or local scope).
Characteristics:
Accessible only within that function.
Not accessible outside the function.
Example:
Block Scope:
What it is: Variables declared with
let
orconst
inside a block (enclosed in{}
) have block scope.Characteristics:
Accessible only within that block.
Block-scoped variables are not hoisted like
var
.
Example:
What is Scope Chain?
The Scope Chain is the mechanism that JavaScript uses to resolve variable names when they are referenced.
When a variable is accessed, JavaScript starts looking for it in the current scope. If it doesn’t find the variable there, it moves up to the next outer scope, and so on, until it reaches the global scope. This series of scopes is what we call the scope chain.
How Scope Chain Works
Current Scope: JavaScript first checks the current scope (where the code is executing).
Outer Scope: If the variable isn’t found, it moves to the immediate outer scope.
Global Scope: If the variable still isn’t found, it checks the global scope.
Uncaught Reference Error: If the variable isn’t found in any of the scopes, a
ReferenceError
is thrown.
Example of Scope Chain
Step-by-Step Explanation:
Inside innerFunction
:
JavaScript first looks for
a
,b
, andc
withininnerFunction
’s scope.It finds
c
ininnerFunction
.For
b
, it doesn't find it ininnerFunction
, so it looks in the outer scope, which isouterFunction
.For
a
, it doesn’t find it ininnerFunction
orouterFunction
, so it looks in the global scope.
This hierarchical lookup process is the scope chain in action.
Lexical Scope
Lexical Scope means that the scope of a variable is determined by its position in the code at the time of writing, not by the context from which it is called. This is why the scope chain follows the nested structure of your code.
Example of Lexical Scope:
Even if you invoke inner
in a different context, it will always have access to the x
variable from outer
because it is defined lexically within outer
.
Arrow functions are a special type of function in JavaScript that differ from regular functions in a few key ways. One of the most important differences is that arrow functions do not have their own this
, arguments
, super
, or new.target
bindings. Instead, they inherit these bindings from the enclosing (lexical) scope.
This behavior makes arrow functions particularly well-suited for certain use cases, such as callbacks or methods that require access to the this
value of the outer scope.
Example 1: Arrow Function in a Lexical Scope
Explanation:
In this example,
innerFunction
is an arrow function that is defined insideouterFunction
.Even though
outerVar
is not defined withininnerFunction
, it is still accessible because of lexical scoping.Since
innerFunction
is lexically insideouterFunction
, it has access toouterVar
even afterouterFunction
has executed.
Example 2: Arrow Function and this
Context
Explanation:
In this example, the
greet
method is an arrow function.Arrow functions do not have their own
this
. Instead, they inheritthis
from the lexical scope in which they are defined.Since
greet
is defined inside thePerson
constructor function, it inheritsthis
from thePerson
instance (person1
), allowing it to accessthis.name
.
Why Lexical Scope Matters with Arrow Functions
The key takeaway is that arrow functions always inherit their surrounding lexical context, including this
. They do not have their own this
, which means they are always bound to the this
of the nearest non-arrow function in the enclosing scope.
Real-World Analogy
Imagine you work in a small company with a clear hierarchy. Every time you need to make a decision (use a variable or access this
), you follow the company policy (lexical scope). If you are unsure, you refer to your immediate supervisor (outer scope). Arrow functions are like employees who always follow their direct supervisor’s instructions, no matter where they are or when they are asked.
Common Use Cases for Arrow Functions and Lexical Scope
Callbacks: When using arrow functions as callbacks, you don’t have to worry about
this
changing unexpectedly.Method Definitions: When defining methods inside objects or classes, arrow functions can ensure that
this
refers to the object or class instance.
Understanding the Difference and Relationship Between Scope Chain and Call Stack in JavaScript
In JavaScript, both the scope chain and the call stack are crucial concepts that help in understanding how the language manages the execution of code. While they are related, they serve different purposes in the execution context.
1. Scope Chain
The scope chain is the mechanism that JavaScript uses to manage variable access and resolution. It is essentially a list of variable objects that the JavaScript engine traverses to find the value of a variable.
How It Works:
When a variable is used, the JavaScript engine looks at the current scope to see if the variable is defined there.
If not found, it moves to the next outer scope and continues this process until it reaches the global scope.
If the variable isn't found in any scope, a
ReferenceError
is thrown.
Example:
function outerFunction() {
const outerVar = 'I am outside!';
function innerFunction() {
const innerVar = 'I am inside!';
console.log(outerVar); // Accesses outerVar via the scope chain
}
innerFunction();
}
outerFunction();
Explanation:
The
innerFunction
has access toouterVar
because of the scope chain, even thoughouterVar
is defined in the outer function. The scope chain links the scopes together, allowinginnerFunction
to "see"outerVar
.
2. Call Stack
The call stack is a data structure that keeps track of function calls in a program. It operates on the "Last In, First Out" (LIFO) principle, meaning the last function that is called is the first one to be executed and removed from the stack.
How It Works:
When a function is called, a new execution context is created and pushed onto the call stack.
When the function finishes executing, its execution context is popped off the stack.
If a function calls another function, the new function's execution context is added on top of the stack.
Example:
function firstFunction() {
console.log('First Function');
secondFunction();
}
function secondFunction() {
console.log('Second Function');
thirdFunction();
}
function thirdFunction() {
console.log('Third Function');
}
firstFunction();
Call Stack Behavior:
firstFunction()
is called, and its execution context is pushed onto the stack.Inside
firstFunction
,secondFunction()
is called, pushing its execution context onto the stack.Inside
secondFunction
,thirdFunction()
is called, pushing its execution context onto the stack.thirdFunction
finishes and is popped off the stack.secondFunction
finishes and is popped off the stack.firstFunction
finishes and is popped off the stack.
3. Relationship Between Scope Chain and Call Stack
Execution Context: Every time a function is invoked, an execution context is created. This context contains information such as the function’s local variables, the this
value, and importantly, the scope chain.
Call Stack Manages Execution Contexts: When a function is called, its execution context is pushed onto the call stack. This execution context has its own scope chain, which includes the function's local variables and its reference to the outer environment.
Scope Chain Used During Execution: As the function executes, whenever a variable is referenced, the JavaScript engine uses the scope chain (part of the execution context) to resolve the variable by traversing through the chain until the variable is found.
Example Illustrating Both Concepts:
Scope Chain in Action: When
innerFunction
executes, the scope chain is used to findglobalVar
andouterVar
. The chain starts frominnerFunction
, moves toouterFunction
, and finally to the global scope if needed.Call Stack in Action: When
outerFunction
is called, its execution context is pushed onto the call stack. WheninnerFunction
is called inside it, a new execution context is pushed onto the stack. The stack helps manage the order of execution, while the scope chain helps resolve variables.
Scope Chain: Determines the accessibility of variables by linking scopes together.
Call Stack: Manages the execution order of functions by pushing and popping execution contexts.
Relation: The call stack manages execution contexts that contain scope chains. The scope chain is used within these contexts to resolve variables as the functions execute in the order dictated by the call stack.
Conclusion
Understanding the core mechanisms of the JavaScript engine, runtime, execution context, call stack, and scope chain is fundamental to mastering the language. The JavaScript engine is responsible for interpreting and executing JavaScript code, while the runtime environment provides the necessary APIs and features that enable JavaScript to interact with its environment, such as the web browser.
The execution context is created whenever a function is invoked or a script is run, encapsulating everything the JavaScript engine needs to keep track of during execution. These execution contexts are organized by the call stack, which dictates the order in which functions are executed. As each function is called, its execution context is pushed onto the call stack; as functions complete, their contexts are popped off, ensuring a controlled and predictable flow of execution.
Meanwhile, the scope chain determines how variables are resolved during execution. Each execution context comes with its own scope chain, which links the current scope to its outer scopes, eventually reaching the global scope. This chain is essential for ensuring that functions can access variables from their outer environments, a behavior known as lexical scoping.
The relationship between the call stack and the scope chain lies in their collaborative role in executing JavaScript code. The call stack manages the order of function executions, while the scope chain ensures that the correct variables are accessed during these executions. Together, they allow JavaScript to handle complex operations efficiently while maintaining a logical structure for variable resolution and function execution.
By grasping these concepts, developers can write more efficient and predictable JavaScript code, better debug issues, and build a stronger foundation for tackling more advanced topics like closures, asynchronous programming, and memory management.
Happy Coding!