Understanding Execution-context, Hoisting, Call-stack, Closures, Scope, Block in JavaScript

Is JavaScript synchronous? Yes.
Is JavaScript single-threaded? Yes.

Enough of jargon like thread and synchronous. Let's understand these terms first.


Synchronous = Sequence = one after other.
var a = 10; //first line
var b = 'Javascript'; //second line

Here synchronous means:
The first variable 'a' is assigned to 10, and then variable 'b' is assigned to 'Javascript.'

The first line is executed, and then the second line is executed.
Single-Threaded = Process one line at a time.
var a = 10; //first line
var b = 'Javascript'; //second line

Here single threaded means:
When the process of assigning 10 to variable 'a' - no other process happens at that time.
Only when the first line finishes the second line starts.

If the first line or process fails, the whole code fails.
Digging Deeper!

Where does the above process take place? This simple two-assignment operation has common steps underneath it.

  • There are two variables, 'a' and 'b.' There must be memory allocated for them to store.

  • The code is executed. i.e. assignment operation to variable 'a' and 'b'.

These steps happen in the 'Execution Context.'- New term? Right!

Execution-context = place or environment where memory creation and code execution take place.

The first phase - Memory creation:

The second phase - Code Execution:

Execution context = memory creation + code execution.

Notice variables a and b are initialized to undefined in the memory phase. Here the variables are initialized to undefined before the code gets executed. We will explain this later.


What if there are functions that use variables and parameters inside?

Does it have an execution context? Yes!.

Is it a separate execution context? Yes!.


Why separate execution context for functions?

Because there are variables and codes inside and outside functions. The function can access variables outside the function but not vice versa. The scope of variables is different.


Therefore two execution contexts - Global and Function execution contexts.

A global execution context is first created when you run a javascript file. When a function is encountered in a javascript file, a brand new execution context is created above the global execution context.


One .js file = one global scope = one Global execution context(GEC)

What about memory allocation for functions?

Memory allocation of any function happens in the parent execution context.

Let's see an example here,


index.js

var a = 10; //first line
var b = 'Javascript'; //second line

//Declaring function
function Test(x){
return x;
};

var c = Test('execution');

When index.js is run, a global execution context is created.

GEC:

Note: Here, the memory allocation of the Test function is created in GEC. Because the parent execution context of the Test function is GEC itself.

When code execution encounters function invocation - Test('execution').It creates Function execution context for Test.

FEC:

Arranging FECs on top of GEC is called Execution stack or Call stack.

When FEC encounters a return statement, it gets out of the FEC and returns to GEC.

When it returns to GEC, FEC is deleted in the Call stack.


What if there are nested functions like callbacks?


function foo(x) {
return x
}

function bar(x){
foo(x)
};

function baz(x){
bar(x)
};

baz('Hello from foo');//Hello from foo

Call stack/Execution stack = Order of processing the execution contexts
Let's define the undefined in the memory creation phase:

We know all variables declared will first be assigned to undefined in the memory phase of the execution context. This assignment is default behavior even before the actual code execution takes place.

For example,

// We can console the variable even before it is declared without any error

console.log(a);
var a = 10;

Before the code executes, variable 'a' is assigned to undefined in the memory phase of the execution context. Therefore above code results in

undefined --> without any error

When var 'a' is not declared, what would be the error?

console.log(a);

It gives a reference error like this.

ReferenceError: a is not defined

So not defined and undefined are different, at least in JavaScript vocabulary.


This idea of the memory creation phase in the execution context before the code execution phase is called 'Hoisting'.

Hoisting = Access the declared variables and functions even before the code execution. It is the result of the memory creation phase in the execution context.

Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution.

Here the scope can be global or function scope.


Diving Deeper into Scoping!

We will illustrate this,

  • Here var a is globally scoped to index.js. It is available to both function one and function two.

  • Function one can access 'a' but not 'c' and 'd'.

  • Function two can access 'a' and 'b' and every variable available to the parent functions and global scope.

What error would we get if we tried to access 'c' or 'd' in global scope?

Is it undefined or not defined? Pause and ponder!

Well, the answer is

ReferenceError: c is not defined

Why? Every function will create separate execution contexts in the call stack. Each has its own memory and code phase.

When you create nested functions like above, the call stack would be,


Two FEC has access to both one FEC and one GEC. This idea of restricting access to the outer functions is called 'Closures.'

Closure = Access to outer scopes.

Closures are used in nested functions or callbacks.

In the call stack, the innermost functions are at the top, and the outermost functions will be at the bottom. The global execution context will be the bottom-most.


Let's Recap before proceeding,

  • JS is a single-threaded synchronous language - one task at a time in sequence order.

  • JS code is implemented in two phases - The memory phase and the code execution phase.

  • The memory phase happens first and then Code execution happens.

  • These two happen in an environment called 'Execution context'.

  • Execution context can be Global or Functional execution context. (GEC and FEC).

  • Variables and functions are moved to the top of the scope using the memory creation phase. This concept is called 'Hoisting'.

  • Ordering of execution context in a stack is called execution stack or call stack.

  • When you nest functions, inner functions will have access to all the outside function's scope. This is called 'Closures'.

Consequences:

Let us dig deeper into var.

Scope of var:

var a = 1;
console.log(a) // prints 1 because var 'a' is global scoped

function print() {
var a = 2;
console.log(a); // prints 2 because var 'a' is function scoped
};

Variables look for the nearest scope and then move outwards to the global scope. Thanks to closures. Why? Function and Global have different execution contexts.

var say = 'Hello'

function name() {
var person = 'John'
console.log(`${say} ${person}`) //Prints Hello John
}

name();

console.log(person) // Error: var person is not defined --> person is in FEC name and it is available only to function name.

Hoisting implementation
console.log(a)
var a = 1;

is interpreted as
var a = undefined; //First memory phase in EC
console.log(a); // prints undefined because of previous step --> This is hoisting.
a = 1 // Now assignment takes place in the code execution phase in EC
Real Problem with Var:

We know that there are two execution contexts - one for Global(which covers the whole .js file) and for each function(as soon as the function is executed, it is removed from the call stack).

This means other than variables inside a function scope(FEC) - all other things like if..else, try..catch will come under the global scope(GEC).Basically two scopes.


var a = 1;
var b = true;
console.log(a); prints 1 because Global scope

if(y) {
var a = 2;
}
console.log(a) 
// prints 2 because values of var 'a' has been changed within the global context - no new execution context is created for 'if' statements unlike 'functions'.
Within the same execution context, var declaration is changeable.
Var can be changed or updated.
During hoisting, var is initialized to undefined. We can access it before initialization.

Larger applications have many variables, if-statements, try-catch, etc. We may accidentally use the same variable name like var 'a' in the above example. This may have unintended consequences in our code which may affect the overall application.


So var declaration has two problems,

  • It is either functionally or globally scoped. Any declaration outside the function will have global scope.

  • It is changeable within its own scope - global or functional.

Birth of let and const:
const takes care of the change problem while let takes care of the scope problem. In fact, both have new scope called 'Block scope' and are not initialized to undefined at first

But wait. What is block here?

Block = anything enclosed by { }
if {
// it is a block
}
else {
// it is a block
}

try {
// it is a block
}
catch {
// it is a block
}

For example,

console.log(a); //  Cannot access 'a' before initialization

const a = 1; // Assignment with const

a = 2; // TypeError: Assignment to constant variable.

const a = 2; // SyntaxError: Identifier 'a' has already been declared

console.log(a); //  Cannot access 'a' before initialization

let a = 1; // Assignment with let

a = 2;  ***//Accepts this assignment operation ***

let a = 2; //SyntaxError: Identifier 'a' has already been declared

We can conclude that const can't change its value once declared while let can change its value after declaration.

Note: Both const and let can't be redeclared. It will give a syntax error.


What about block scope in let and const?

Block = anything within two curly braces { }.Therefore this includes functions.


const a = 1;
let b = 1;

if(true) {
...block starts

const a =2;
console.log(a) // Prints 2 because of block scope for const.

let b = 3;
console.log(b) // Prints 3 because of block scope for let.

block end...
}

// Within the block, there must not be the same variable names!!

function testing () {
const a =2;
console.log(a) // Prints 2 because of block scope for const.

let b = 3;
console.log(b) // Prints 3 because of block scope for let.
}

Are the let and const hoisted?

Yes! They are hoisted - moved top of the scope but not initialized to undefined. This is the reason you get 'cannot access before initialization'. If it is not hoisted, it would be 'not defined.'