JavaScript: Demystify Hoisting in JS
Tags: javascript, frontend, hoisting, webdev
Published: 2022-08-29 06:09:13

## 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?

```js
console.log(a);
// ReferenceError: a is not defined
```

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

```js
console.log(a); // undefined
var a;
```

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:

```js
var a;
console.log(a); // undefined
```

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?

```js
console.log(a); // undefined
var a = 10;
```

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:

```js
var a;
console.log(a); // undefined
a = 10;
```

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:

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

func(10);
```

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

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

func(10);
```

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:

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

func(10);
```

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:

```js
var a = 10;
var a;
console.log(a); // 10
```

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

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

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:

```js
console.log(a);
var a;
function a() {}
```

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

```js
var a;
console.log(a);
function a() {}
```

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:

```js
// Prioritized
function a() {}
var a;
console.log(a);
```

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
```

## 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.

```js
console.log(a);
let a;
// ReferenceError: a is not defined
```

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:

```js
var a = 10;
function func() {
  console.log(a);
  let a;
}
```

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:

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

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

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g6886vrdtam8ngw9kxxc.png) 

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
```

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:

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

func(10)
```

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

```
// VO
{
    a: 10,
    b: undefined,
    c: undefined
}
```

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?

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

func(10)
```

The `VO` looks like this:

```
{
    a: function a
}
```

So we can know that `function declarations`will 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.
```

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

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

func(10);
```

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
}
```

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?

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

func(10);
```
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!