DEV Community

Cover image for JavaScript Hoisting - behind the scenes
Shubham Kumar
Shubham Kumar

Posted on

JavaScript Hoisting - behind the scenes

In this post, I want to talk about how hoisting mechanism occurs in JavaScript.Before we dive in, let’s define what hoisting is.

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

Consider this code. Can you guess what is the output at line 3? :

a = 2;
var a;
console.log( a );
Enter fullscreen mode Exit fullscreen mode

Many of us would expect it as 'undefined' , since the 'var a' statement comes after the a = 2 , and it would seem natural to assume that the variable is redefined, and thus assigned the default undefined . However, the output will be 2, due to hoisting.

Hence,hositing allows us to use variables and call functions before even writing them in our code. Now this can be very confusing if you are coming from a different language like Java, where you need to define a variable first before using them.

Let's consider another example, can you guess the output now?:

console.log( a );
var a = 2;
Enter fullscreen mode Exit fullscreen mode

The output here will be undefined. This goes on to show that the hoisting mechanism only moves the declaration. The assignments are left in place.

But, this doesn't mean that during the compilation the JS Engine magically restructures your code to move all the declarations to the top of the scope. This behaviour is a result of the two phases that the program goes through - the compilation and the execution phase.

During compilation phase, the code lexing and tokenizing phenomenon occurs. This simply means it will split our code into atomic tokens like a, = and 2.(A token is a single element of a programming language).
Note that no assignment or evaluation takes place during this phase.

Each time the compiler encounters a declaration, it sends it to the scope manager to create the binding. For each declaration it allocates memory for that variable. Just allocates memory, doesn’t modify the code to push the declaration up in the codebase. And as you know, in JS, allocating memory means setting the default value undefined.

After the compilation phase we go to execution phase, Where each time the engine encounters an assignment or an evaluation(e.g a function call/expression evaluation), it asks the scope for the binding. If not found in the current scope, it goes up into parent scopes until it finds it, and then the evaluation is done with that binding.

Hence the second snippet is excecuted as follows:

Compilation phase or first pass:

console.log(a) // skipped as it is an evaluation

var a = 2; 
/* 
This has two parts -
    1. A declaration part: var a 
    2. An assignment part: a = 2. 
The compiler only deals with the declaration part, 
and allocates memory for variable 'a'. 
The assignment will happen in excecution phase.
*/

Execution phase or second pass:

console.log(a)
/*
console.log() function called with a. 
The engine looks for the variable 'a' in the scope, and finds it,
for now has the value undefined, so prints it.
*/
var a = 2;  
/* 
The engine executes the assignment operation.
Looks for the variable 'a' in the scope chain and finds it.
Assign 2 to it.
*/
Enter fullscreen mode Exit fullscreen mode

This also happens with function declarations. Let's look at this example.:

foo();
function foo() {
    console.log( a );
    var a = 2;
}

// Compilation Phase:
foo(); // As this is evaluation, this line is skipped

function foo() {
    console.log( a );
    var a = 2;
}
/* 
The complier sees a declaration with identifier foo, hence memory is allocated to it
As it is a function and a new scope is also created.
It then again encounters a declaration for an indentifier a, so it allocates it to the memory.
*/

// Excecution Phase:
foo();
/* 
The engine looks for the identifier foo in the
scope chain. 
It finds and pull out the value that foo is referencing to- the statements inside it.
() executes the function,and the excecution moves inside foo function
It encounters a call to console.log() with argument a, which at this time is 'undefined'.
It prints undefined.
Execution moves to next line and encouters an assignment.
It looks for identifier a in the function scope and assigns value 2 to it.
The execution moves outside to global scope.
There are no more execution statements, so the program stops.
*/
Enter fullscreen mode Exit fullscreen mode

We should also note that, Function declarations are hoisted, as we just saw, but function expressions are not. The reason being, assignment is done during execution phase.

foo(); // not ReferenceError, but TypeError!
var foo = function bar() {
    console.log('Inside bar')
};
Enter fullscreen mode Exit fullscreen mode

Here, the variable identifier foo is hoisted and attached to the global scope, so foo() doesn’t fail as a ReferenceError .
But foo has no value yet, so foo() is attempting to invoke the undefined value, which is a illegal operation. Hence it throws TypeError

Note: Both function declarations and variable declarations are hoisted, but as function declarations take precedence to variable declarations, functions are hoisted first and then variables.

This can be illustrated by below snippet:

foo(); // Output is: 1
var foo;
function foo() {
    console.log( 1 );
}
foo = function() {
    console.log( 2 );
};
Enter fullscreen mode Exit fullscreen mode

Hositing with let and const

Variables declared with let and const are also hoisted, but unlike for var the variables are not initialized with a default value of undefined. Until the line in which they are initialized is executed, any code that accesses these variables will throw an exception.

Variables declared with the keyword let are block scoped and not function scoped. It just means that the variable’s scope is bound to the block in which it is declared and hence during compile time, memory is allocated for the variable declared with let, but it is initialized to a value only when a parser evaluates it.

Note: Unlike var, accessing the variable declared as let or const before the initialization results in a ReferenceError. For var variables, they return a value of undefined if they are accessed before they are declared.

Hence, the below code snippet will return ReferenceError

console.log(a); 
// Output: ReferenceError: a is not defined ...
console.log(b);
// Output: ReferenceError: b is not defined ...
let a = 10;
const b = 11;
Enter fullscreen mode Exit fullscreen mode

To conclude, we can say that a JS program is parsed and executed in two passes, because of which concept of hoisting comes in to picture.

Discussion (0)