How JavaScript hoisting works under the hood

How JavaScript hoisting works under the hood

Written by steven | Published 2 months ago

JavaScript is weird.

Not syntactically. But how it behaves under the hood.

And it often confuses a lot of newcomers or developers coming from different programming languages. And until and unless you understand why it works the way it works, you’ll meet the confusion and difficulties the same way.

One of those seemingly strange phenomena in JS, that often surprises many, is - Hoisting.

But before jumping onto understanding what hoisting is, you first need to be familiarized with what is known as Execution Context. If you’ve never heard the term before, Execution Context (EC) is defined as the context or the environment in which variables, classes, or functions will be executed. When you write something like this:

 var x = 2; 
 function myFunc(){
    const y = 2; 
 }

It means that variable x and function myFunc are sharing an EC within which they will be created, and executed. They are part of the global execution context (GEC), which is already declared for you by the JS engine as soon as it loads your file in the browser (or in the local environment).

Also, the myFunc function has its EC within which variable y is declared, and to expose this function-created execution context (FEC) to the JS engine, myFunc needs to be invoked. That means each function invocation creates an EC.

So what happens with those individual execution contexts - global or those created by function invocations?

They simply pile up on top of one another in the Execution Context Stack, in the order of their invocation, which tells the JS engine in what order things will be executed.

The code in each EC goes through two stages:

  • Creation stage
  • Execution stage

That means, each variable, or function in a particular EC first gets created and then executed. And as soon as the execution stage ends, the EC is popped out of the execution context stack and the code of the next EC in the stack goes through the same set of stages mentioned above. This process continues in this manner until the stack is empty.

Look at this example below:

 var a = [1, 2, 4];
 
 function doSomething(){
    // does something
 }
 doSomething(); 

As soon as the file loads in the browser, GEC is created.

In the creation stage, the variable ‘a’ is allocated a memory space, initialized with ‘undefined’ and the whole function doSomething gets memory allocation as well. In the execution stage, the variable ‘a’ is reassigned to an array and the function doSomething gets invoked. And that function invocation stacks up doSomething’s separate EC. Now, here is the exercise for you. Look at the code below and try to guess whether this will be allowed by the JS engine or it will throw an error.

 a = [1, 2, 4];
 doSomething();
 
 var a;
 function doSomething(){
    // does something
 } 

If you have been following this article closely, you’re more likely to decode it using a separate creation stage and an execution stage and conclude, “Yeah! It will work.”

And you’re right. This is allowed in JS. And that is what hoisting is, which in simpler words means, “Creating memory location for all the variables and function declarations in the creation stage itself so that when in the execution stage those variables need reassignment or accessing, the JS engine knows them.

A lot of people, who miss to understand execution context, confuse hoisting to be some magical phenomenon performed by JS engine where the engine physically moves code to the top. But now you know, that it’s not the code that moves to the top, but the creation stage that does the necessary hoisting occurs before the execution of code. Now, what do you think about the declaration below? Does this hoist too?

 x = 2; 
 let x;  

To a surprise to many, this code throws an error, which goes like: 

Uncaught ReferenceError: Cannot access 'x' before initialization 

Let’s see what’s happening in the code-piece above.

In the creation stage, the x is declared using the ‘let’ keyword but is not initialized. And that is the case with all the declarations made with let, const, and class introduced in ECMAScript-6 (ES6)

That means, in this stage, declaration of variables happens, a distinct memory is allocated, but without any initial value assignment.

And that is why, in the execution stage, the accessing or reassignment of that variable is not allowed before any initialization.

And the initialization can only happen after the declared statement, that is only after the line: 

let x;  

Not before it.

The zone above this line of declaration for variable x is known as Temporal Dead Zone (TDZ). This TDZ disallows the reassignment and initialization in the execution stage, and hence the declaration above does behave differently. 

But the declaration made with the ‘var’ keyword gets initialized in the creation stage itself, hence doesn't have any TDZ. 

// Temporal Dead Zone
let x; 
// Zone allowed for initialization and reassignment. 

Therefore, it’s not the hoisting problem with declarations made with let, const and class keywords, but the initialization problem that makes them behave differently than the declarations made with var, and function keywords.

Now, as the last exercise, what do you think the console below prints?

let y; 
console.log(y);

If you’ve answered it to be ‘undefined’, you are right.

Here is how. 

The creation stage declares the variable y. 

And in the execution stage console statement lies after the TDZ, which allows initialization and reassignment. So y is automatically assigned an initial value of ‘undefined’ past the TDZ if no other value is assigned to it.

It’s that simple. Isn’t it?

Related blogs