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
We find that the browser will prompt you with areference
error, telling you thata
is not yet defined. But what if?
console.log(a);// undefinedvara;
How can this happen? Logically speaking, the code runs line by line, and the error should beReferenceError
? Why is itundefined
?
This is because of the hoisting of JS, state lifting. Lifting is reflected in the fact that the linevar a
is "lifted" to the top. So in fact, when the JS engine(V8) parses this code, it actually works like this:
vara;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?
console.log(a);// undefinedvara=10;
The is stillundefined
, but why? Shouldn'tvar a = 10
be lifted to the top, thus logginga
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:
vara;console.log(a);// undefineda=10;
So in fact,var a = 10
can be decomposed into two steps. First,var = a
will be lifted first, which is thedeclaration
part, and the second stepa = 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:
functionfunc(h){console.log(h);varh=3;}func(10);
I thought it was ridiculous at first, what is there to say, isn't it?
functionfunc(h){varh;console.log(h);h=3;}func(10);
So of course the output will beundefined
! Well, the actual result immediately slapped in the face, and10
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:
functionfunc(h){varh=10;varh;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 ofh
without assignment. So it should still beundefined
?
Well, let's try a more straightforward example:
vara=10;vara;console.log(a);// 10
Using the above decomposition method, it can actually be seen as this:
vara;vara;a=10;console.log(a);
This outputs10
, 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);vara;functiona(){}
According to the decomposition method above, we want to decompose it into this:
vara;console.log(a);functiona(){}
The output should beundefined
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 invariable 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:
// Prioritizedfunctiona(){}vara;console.log(a);
Having said so much, let’s sort it out a little:
1. Both variable declarations and function declarations will participate hoisting processes2. Variables are promoted only by declaration, not by assignment3. 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 keywordvar
was used to declare variables. However, everyone knows that ES6 introducedlet
andconst
, and the mainstream no longer recommends the use of var, but instead uses let with const.
Forlet and const
, in fact, the concept of hoisting is similar. Let's take a look at some examples here.
console.log(a);leta;// ReferenceError: a is not defined
Magic! The output isReferenceError: 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 usinglet
to declare functions? If so, it would be great, but unfortunately, it is not. Take a look at the example below:
vara=10;functionfunc(){console.log(a);leta;}
Iflet
has no state hoisting, then the output should be 10, right? Because there isvar a = 10
outside, and thenlet a
has no hoisting. Wrong again, the output isReferenceError: a is not defined
. So in fact,let
is also hoisted, but the process behavior oflet
promotion may be different from that ofvar
, 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 oflet 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.
- Why hoisting?
- 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?"
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.
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?
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:
functionloop_1(){console.log("loop 1");loop_2();}functionloop_2(){console.log("loop 2");loop_1();}
This code is not difficult to understand.loop_1
andloop_2
call each other. But there is a problem, if there is no hoisting, how canloop_1
be aboveloop_2
, and at the same time,loop_2
is also aboveloop_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, theExecution Context
of JavaScript, which is abbreviated asEC
below. The concept ofEC
is that every time a function is entered, the function will have anEC
, and then theEC
will be pushed onto the stack. When the function is executed, the EC will be popped out.
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.
EachEC
has a correspondingVO (Variable Object)
. ThisVO
is what stores all the information, including the variables in the function, the function, and the parameters in the function. The mechanism for searching theVO
means that:
Takevar a = 10
above as an example, the first step is to add a new attributea
inVO
, and then find the attribute nameda
and set it to 10.
Step1: var aStep2: a = 10
Well, there are so many things in a function, how does it work to put everything into theVO
of eachEC
?
For parameters, it will be directly put into theVO
, if some parameters aren't passed with a value, then its value will be initialized to becomeundefined
. Consider the following example:
functionfunc(a,b,c){......}func(10)
The above function if called, theVO
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?
functionfunc(a){functiona(){......}}func(10)
TheVO
looks like this:
{ a: function a}
So we can know thatfunction declarations
will take priority over thevariable 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:
functionfunc(h){console.log(h);varh=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 toconsole.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?
functionfunc(h){console.log(h);varh=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!
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse