Hii ๐ everyone, hope you all are doing good. Today we are going to explore some of the important topics in JavaScript, which will help us to understand behind the scene working of the javascript in a web browser.
- Execution context
- Hoisting
- Scope
- Call Stack
- Synchronous and Single-Threaded
How JavaScript Code Gets Executed?
The web browser does not directly understand the high-level javascript code that we write in our applications. Not only in a web browser but in any other programming language the CPU does not understand the high-level code, It first needs to be converted into such a language that the CPU, in our case a web browser can understand - machine code.
While scanning through the HTML
file, if the browser finds any javascript code via <script>
tag it will send this script to its javascript engine which will then create a special environment for the transformation and execution of the code, this environment is called Execution Context.
The execution context contains the code which is currently running and everything necessary to aid its execution.
There are two kinds of Execution Context in JavaScript:
- Global Execution Context (GEC)
- Function Execution Context (FEC)
Global Execution Context (GEC)
Whenever a JavaScript engine encounters a script file, it first scan the whole file and stores all the variables and functions inside different memory allocations in the global scope (space), which will be executing in the execution phase. if there is a variable it will be given a different memory allocation inside the GEC and if there is a function, the whole function declaration will be given a different memory allocation. This scanning of the file and allocating the memory based on the type is known as hoisting.
For every JavaScript file there can be only one GEC.
Function Execution Context (FEC)
Whenever a function is called, the JavaScript engine creates a different type of Execution Context known as a Function Execution Context (FEC) within the GEC to evaluate and execute the code within that function. Since every function call gets its own FEC, there can be more than one FEC in the run-time of a script.
let's understand this with an example.
let a = 10; // In global scope
function forExample(){
let b = 20; // In function scope
console.log(b);
}
forExample();
So what happens is that during the scanning phase it will store the variable a
in a memory allocation and
whole function declaration forExample
in a different memory allocation, and since the execution phase is not started yet so the value of the variable a
will be assigned as undefined
.
And during the execution phase, a value of 10 will be assigned to a
. and a new execution context - functional execution is created which again creates a GEC inside it and the value of the variable b
becomes undefined, and during functional execution context of the function, b
will be assigned 20.
This process of storing variables and function declaration in memory prior to the execution of the code is known as Hoisting.
Hoisting in JavaScript
Function and variable declarations are hoisted in JavaScript. This means that they are stored in the memory of the current execution Context as discussed above and made available within the Execution Context even before the execution of the code begins. This allows functions to be used in the code before they are declared.
Variable Hoisting
In variable hoisting, we can use a variable before it is declared or initialized. But the most important thing is that only variable declarations are hoisted not the initializations. For variables declared using the var
keyword the default initialization is undefined
and otherwise no initialization. Below is the use case of this scenario.
In this example first we have declared the variable and then initialized it after using it.
console.log(temp); // Output: undefined, because we are using it before it is actually initialized
var temp; // Declaration
temp = 10; // Initialization
console.log(temp); // Output: 10, because we are using it after initializing it.
The same thing will happen if we declare and initialize the variable in the same line as shown below.
console.log(temp); // Output: undefined, because we are using it before it is actually initialized
var temp = 10; // Declaration and Initialization in the same line
console.log(temp); // Output: 10, because we are using it after initializing it.
If we don't declare the variable and only initialize it, this will give a reference error because the variable is not hoisted in this case.
console.log(temp); // Reference Error: temp is not defined
temp = 10; // Initialization
Although the variable is not hoisted in the above case but if we use the variable after its initialization the code will work fine.
temp = 10; // Initialization
console.log(temp); // Output: 10
Let and Const Hoisting
Variable declared with the Let
and const
keywords are also hoisted but unlike var
they are not initialized with the default value: undefined
. An error will occur when we access any variable declared with the let
and const
keywords before it is initialized.
console.log(temp); // Reference Error: as the variable is not initialized
let temp = 10; // Initialization
class Hoisting
Classes defined using class declaration are hoisted but are not initialized by default. if we use any class
before it is defined will throw a ReferenceError
as shown below.
let saini = new Student("saini", 19) // ReferenceError: Cannot access 'Student' before initialization
console.log(saini .name);
class Student {
constructor(name, roll){
this.name = name;
this.roll = roll;
}
}
Correct way is to use class after it is declared and defined.
class Student {
constructor(name, roll){
this.name = name;
this.roll = roll;
}
}
let shiva = new Student("shivanand", 10)
console.log(shiva.name); // Output: shivanand
Function Hoisting
A function declared using the function
keyword will be hoisted by JavaScript and will be made available anywhere in the code irrespective of the position of the function declaration
.
iamHoisted(); //hoisted
function iamHoisted(){
console.log("hoisted");
}
iamHoisted(); //hoisted
However, if we assign a variable a function (functional programming) then in this case the variable declaration is hoisted and the expression (function definition) is its initialization. Therefore the expressions are not evaluated until the relevant line is executed. this is same as variable hoisting.
let temp1 = function (){
console.log("Am I Hoisted?"); //Am I Hoisted?
}
temp1();
temp2();
let temp2 = function (){
console.log("Am I Hoisted?"); //ReferenceError: Cannot access 'temp2' before initialization
}
So now let's discuss about scope
in JavaScript.
Scope
Scope in general terms means where to look for things and what it is that we are looking for. It is the current context of execution in which variables and expressions are made available for use or can be referenced. And if the variable is not available for use it can't be used. Further, a scope can be hierarchically chained in one another so that the child scope can access the parent scope but not vice versa.
Types of scope
- Global Scope: This is the default scope for all the code running in the JavaScript.
- Module Scope: The scope for the code running in the module mode.
- Function Scope: The scope created by the function.
- Block Scope: The scope created within the curly braces
{ }
.
Function Scope
A function creates the scope so that the variable declared inside the function can't be accessed from outside the function and within other functions as shown below.
function forExample (){
let temp = 10; // exclusively for forExample function
console.log(temp); //10
}
forExample();
console.log(temp); //ReferenceError: temp is not defined
function forExample (){
var temp = 10; // exclusively for forExample()
console.log(temp); //10
}
function forExample2 (){
console.log(temp); //ReferenceError: temp is not defined
}
forExample();
forExample2();
console.log(temp); //ReferenceError: temp is not defined
Scope Chaining and Lexical Scope
The nesting of the scope inside different functions is called as scope chaining.
function parent (){
var moneyParent = 100;
console.log(moneyParent); // 100
console.log(ageChild); // ReferenceError: ageChild is not defined
console.log(ageGrandChild); // ReferenceError: ageGrandChild is not defined
function child (){
var ageChild = 30;
var moneyChild = 50;
console.log(moneyParent); // 100
console.log(moneyChild); // 50
console.log(ageChild); // 30
console.log(ageGrandChild); // ReferenceError: ageGrandChild is not defined
function grandChild(){
var ageGrandChild = 8;
var moneyGrandChild = 1;
console.log(ageChild); // 30
console.log(ageGrandChild); // 8
console.log(moneyParent); // 100
console.log(moneyChild); // 50
console.log(moneyGrandChild); // 1
}
grandChild();
}
child();
}
parent();
In the above example, we can see that the parent
function variables and child
function variables can be accessed inside the grandChild
function, but the reverse is not possible this is called scope chaining.
and we can also say that grandChild is in lexical scope with child, and child is in lexical scope with the parent, and parent is in lexical scope with global which is called global scope.
Block Scope
The scope created with the pair of curly braces { }
while using the let
and const
keywords.
{
var temp = 10;
}
console.log(temp); // 10
{
const temp = 10;
}
console.log(temp); // ReferenceError: temp is not defined
{
let temp = 10;
}
console.log(temp); // ReferenceError: temp is not defined
Call Stack
A call stack is almost like a mechanism by which the interpreter of the web browser keeps track of its place in the JavaScript code that makes multiple calls or has functions that call another function. As what function is currently being run and how many functions calls it is making from that function and so on. The working is like this:
First, when the script calls the function the interpreter places it on the top of the GEC (Global Execution Context) and then start executing the function.
During the execution of the first function if this function is making another function calls from within, the call stack add that call further up in the call stack (above the call of first function) and start executing it.
When the execution of the current function is done, the call stack removes it from the stack and returns the code execution flow to the point where it left off in the last code listing.
After returning to the point where it left off in the last code listing, it executes the remaining code and returns the execution flow to GEC.
Finally call stack removes that function also from the call stack and GEC takes over the control.
function firstFunction(){
// Some code
console.log("Inside firstFunction");
secondFunction();
// Some code
console.log("Again Inside firstFunction");
}
firstFunction();
function secondFunction(){
console.log("Inside Second function");
}
// Output:
// Inside firstFunction
// Inside Second function
// Again Inside firstFunction
Below diagram will help in understanding this.
Last one โ๏ธ
Synchronous and Single-Threaded
Synchronous simply means that the script will get executed line by line after completing each task (sequence). And if one task is taking more time to complete the task it will wait for the task to get done and then move to the next task.
Single-threaded means that the JavaScript engine only has one call stack to do all the tasks. Let's understand this by an example
Case-1
console.log("1");
for(let i = 1; i<10000; i++){
// a loop that takes 5 seconds to complete
}
console.log("2");
Output:
1
2 //after 5 seconds
Case-2
console.log("1");
setTimeout(() =>{
// fetching web api..
console.log("Web api..")
},5000)
console.log("2");
Output:
1
2
Web api.. // after 5 seconds
If we have something like this then in case-1 the 2
will only be printed as soon as the task of the for
loop is completed i.e after 5 seconds, this behavior of executing code line by line after completion of the task is called synchronous behavior. Whereas the code in case-2, the output 2 will be printed instantaneously because setTimeout
is a browser feature that is deferred and the function inside it is called after five seconds has been completed. Therefore certain browser features and third-party libraries that access the network are async in nature so that in the meantime the request (task) is getting fulfilled our JS engines have some task to perform.
That's all for now, let's meet again with some other article until then keep learning.