DEV Community

cclintris
cclintris

Posted on

JavaScript: Demystify Hoisting in JS

Intro

One of the biggest reasons for writing this article is that I once heard several co-workers talking about hoisting in JS, which is, state lifting. It was really difficult for me to understand their perspectives at that time, so I started to search for related articles on the Internet. It turned out that this does not seem to be something that can be figured out in a moment, so I started this blog, hoping to crack another knowledge point of JS.

What is Hoisting actually?

Without further ado, let's look at an example. What happens if you try to access a variable that hasn't been declared?

console.log(a);
// ReferenceError: a is not defined
Enter fullscreen mode Exit fullscreen mode

We find that the browser will prompt you with a reference error, telling you that a is not yet defined. But what if?

console.log(a); // undefined
var a;
Enter fullscreen mode Exit fullscreen mode

How can this happen? Logically speaking, the code runs line by line, and the error should be ReferenceError? Why is it undefined?

This is because of the hoisting of JS, state lifting. Lifting is reflected in the fact that the line var a is "lifted" to the top. So in fact, when the JS engine(V8) parses this code, it actually works like this:

var a;
console.log(a); // undefined
Enter fullscreen mode Exit fullscreen mode

But note! It is just imagination, not that the JS engine physically moved the code!

So what if I change the code to look like this?

console.log(a); // undefined
var a = 10;
Enter fullscreen mode Exit fullscreen mode

The is still undefined, but why? Shouldn't var a = 10 be lifted to the top, thus logging a out should be 10?

Well another concept is introduced here: only the declaration of variables will be promoted, and the assignment will not.

So actually the code above looks like this in the eyes of JS engines:

var a;
console.log(a); // undefined
a = 10;
Enter fullscreen mode Exit fullscreen mode

So in fact, var a = 10 can be decomposed into two steps. First, var = a will be lifted first, which is the declaration part, and the second step a = 10 will stay and not participate in the hoisting process.

For now, everything is easy, but then the chaos may start. Take a look at the following example:

function func(h) {
  console.log(h);
  var h = 3;
}

func(10);
Enter fullscreen mode Exit fullscreen mode

I thought it was ridiculous at first, what is there to say, isn't it?

function func(h) {
  var h;
  console.log(h);
  h = 3;
}

func(10);
Enter fullscreen mode Exit fullscreen mode

So of course the output will be undefined! Well, the actual result immediately slapped in the face, and 10 is the output.

In fact, the process of conversion and hoisting is correct, but the only thing is we forgot to call the function. So it's actually like this:

function func(h) {
  var h = 10;
  var h;
  console.log(h);
  h = 3;
}

func(10);
Enter fullscreen mode Exit fullscreen mode

But it's still a bit strange, because even so, var h is called again before taking the value of h without assignment. So it should still be undefined?

Well, let's try a more straightforward example:

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

Using the above decomposition method, it can actually be seen as this:

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

This outputs 10, which is acceptable. At the time, my feeling was, what the hell is going on! Where did so many unreasonable rules just come from? Who can figure it out? Just first bear with it and look at one last example:

console.log(a);
var a;
function a() {}
Enter fullscreen mode Exit fullscreen mode

According to the decomposition method above, we want to decompose it into this:

var a;
console.log(a);
function a() {}
Enter fullscreen mode Exit fullscreen mode

The output should be undefined right? The result is another big slap in the face, outputting [Function: a]. At this point I was already hitting myself. It turns out that in addition to the concept of state lifting in variable declaration assignments, function declaration also applies. Moreover, the matching priority of state promotion of function declaration is higher than that of variable declaration. So, in fact, the above code should be imagined like this:

// Prioritized
function a() {}
var a;
console.log(a);
Enter fullscreen mode Exit fullscreen mode

Having said so much, let’s sort it out a little:

1. Both variable declarations and function declarations will participate hoisting processes

2. Variables are promoted only by declaration, not by assignment

3. Don't forget that there are also passed parameters in the function
Enter fullscreen mode Exit fullscreen mode

let const & hoisting

Have you noticed that when the concept of hoisting was introduced above, the keyword var was used to declare variables. However, everyone knows that ES6 introduced let and const, and the mainstream no longer recommends the use of var, but instead uses let with const.

For let and const, in fact, the concept of hoisting is similar. Let's take a look at some examples here.

console.log(a);
let a;
// ReferenceError: a is not defined
Enter fullscreen mode Exit fullscreen mode

Magic! The output is ReferenceError: a is not defined. Well, this is actually more common sense, right? But does that mean that there is no such thing as hoisting when using let to declare functions? If so, it would be great, but unfortunately, it is not. Take a look at the example below:

var a = 10;
function func() {
  console.log(a);
  let a;
}
Enter fullscreen mode Exit fullscreen mode

If let has no state hoisting, then the output should be 10, right? Because there is var a = 10 outside, and then let a has no hoisting. Wrong again, the output is ReferenceError: a is not defined. So in fact, let is also hoisted, but the process behavior of let promotion may be different from that of var, so it seems that there is no hoisting at first glance. As for how it works, we will discuss it later.

Pause here. If you just want to know just a little bit about what hoisting is, you can actually stop here, because in fact, make good use and take advantage of let const, and then declare assign your variables properly, you'll be just fine. But if you want to understand more thoroughly and deeper, just stick with me and keep reading. Next, we first discuss two important questions about hoisting.

  1. Why hoisting?
  2. How exactly does hoisting work?

Why hoisting?

Reviewing some of the rules and concepts of hoisting mentioned above, we can feel some of the benefits it brings. To answer this question, you can think from the opposite angle: "What would happen if there was no hoisting?"

  1. Without hoisting, we would have to declare a variable before using it. But this is actually very good. After all, everyone writes like this when programming. I suppose no one will code and at meantime have the thought of JS's hoisting mechanism, then don't declare variables and use them directly, right? So this is actually good.

  2. Without hoisting, it also stipulates that when we use a function, it must be declared and defined above. At first glance, it seems that there is nothing wrong with it, but it is actually a little troublesome. Because, this means that only by putting every function on top, can you completely ensure that any function called below can be executed normally. Now that's a little messy, isn't it?

  3. The last point is more interesting. Without hoisting, we wouldn't be able to call each other between functions. What does this mean? Take a look at the code below:

function loop_1() {
  console.log("loop 1");
  loop_2();
}

function loop_2() {
  console.log("loop 2");
  loop_1();
}
Enter fullscreen mode Exit fullscreen mode

This code is not difficult to understand. loop_1 and loop_2 call each other. But there is a problem, if there is no hoisting, how can loop_1 be above loop_2, and at the same time, loop_2 is also above loop_1. This code wouldn't work without hoisting.

As a conclusion, hoisting is to solve these problems!

How exactly does hoisting work?

First of all, we need to introduce a concept, the Execution Context of JavaScript, which is abbreviated as EC below. The concept of EC is that every time a function is entered, the function will have an EC, and then the EC will be pushed onto the stack. When the function is executed, the EC will be popped out.

Image description

In general, EC stores the information of their respective functions. When the function needs something, it will go to its own EC to find it.

Each EC has a corresponding VO (Variable Object). This VO is what stores all the information, including the variables in the function, the function, and the parameters in the function. The mechanism for searching the VO means that:

Take var a = 10 above as an example, the first step is to add a new attribute a in VO, and then find the attribute named a and set it to 10.

Step1: var a
Step2: a = 10
Enter fullscreen mode Exit fullscreen mode

Well, there are so many things in a function, how does it work to put everything into the VO of each EC?

For parameters, it will be directly put into the VO, if some parameters aren't passed with a value, then its value will be initialized to become undefined. Consider the following example:

function func(a, b, c) {
    ...
    ...
}

func(10)
Enter fullscreen mode Exit fullscreen mode

The above function if called, the VO will look like this:

// VO
{
    a: 10,
    b: undefined,
    c: undefined
}
Enter fullscreen mode Exit fullscreen mode

If there is another function declaration in the function, it is also added to the VO, no problem. But what if the name of the function is coincidentally same as a variable name?

function func(a) {
    function a() {
        ...
        ...
    }
}

func(10)
Enter fullscreen mode Exit fullscreen mode

The VO looks like this:

{
    a: function a
}
Enter fullscreen mode Exit fullscreen mode

So we can know that function declarationswill take priority over the variable declarations, just like the example above, the parameter a will be overwritten by the function a.

For the variable declaration inside the function, it will be put into the VO at the end. If there is already an attribute with the same name in the VO, the variable will be ignored directly, and the original value will not be modified.

To recap, we can think of the action of the VO mentioned above as a prerequisite work before executing a function. The order is as follows:

Step1: Put the parameters into VO, and then see if there are any incoming values respectively. The parameters are matched in the order in which they are declared. If they are not matched, they will be assigned the value undefined.

Step2: Find the member methods in the function, in other words, other functions, and put it into the VO. If it has the same name as any property in the current VO, overwrite the old one.

Step3: Finally, find the variable declaration in the function and put it in VO. If it has the same name as any property in the current VO, the current state will prevail.
Enter fullscreen mode Exit fullscreen mode

Having said so much, let's come back to the example we mentioned above:

function func(h) {
  console.log(h);
  var h = 3;
}

func(10);
Enter fullscreen mode Exit fullscreen mode

So the execution of each function can actually be divided into two stages. First, it will enter the Execution Context of the function, and then start preparing its own VO. For the above example, first of all, because there are parameters in the call, a variable called h will be declared in VO first, and the value will be 10. Then because the member function is not found in the function, it remains unchanged. Finally find var h = 3, it is a variable declaration statement, so it should be added to VO, but because the VO at this time is already a variable called h, so VO does not change. So far, the VO of this function has been established.

// func() VO
{
    h: 10
}
Enter fullscreen mode Exit fullscreen mode

After creating the VO, start executing this function. When the code executes to console.log(h), it'll look up VO and find that there is a variable called h with a value of 10, so 10 it is. So the above question is answered, it is indeed output 10 Yes!

What if the code was changed to this?

function func(h) {
  console.log(h);
  var h = 3;
  console.log(h);
}

func(10);
Enter fullscreen mode Exit fullscreen mode

The first output will be 10 of course, and the second output will be 3.

In fact, the process of establishing VO is the same as above, so the first output will be 10 when executing, no problem. And because it changes the h in VO when it executes to line 3, the second output will of course be 3!

Top comments (0)