How JavaScript hoisting actually works
You probably do not know what really is happening
Wednesday, October 12, 2022
What we used to know
According to w3schools
"Hoisting is JavaScript's default behavior of moving declarations to the top."
If we take that description literally, given the following code
.js12345JavaScript will rearrange the code for us to this one.
.js12345Or, with a slightly more complex example
.js12345678910111213141516171819202122JavaScript will magically rearrange the variables and function declarations; then, it will run from top to bottom.
.js12345678910111213141516171819202122232425That's what I used to believe, and it's a good mental model, but it's not accurate.
If you think that
letandconstwill solve the issue because they do not hoist, you are in for a treat.
What is hoisting?
Unfortunately, no hoisting JavaScript exists, as in the magical movement of declarations to the top. The word hoist does not even exist in the old ECMAScript specification.
Hoisting is more of a mental model to help us understand how JavaScript let us use a function or variable before they are declared.
So what now?
As an alternative mental model, think that JavaScript is doing two phases before we get the output.
- Creation Phase - When we put our declarations in the Environment Record
- Execution Phase - When we run line by line to do what we already know(creating variables, running functions, etc.)
Let's try running this code using the alternative approach.
.js12345678910111213141516171819202122Output
undefined "world" "hello" "world" "goodbye" "WORLD!"
Creation Phase

- Start with an empty global scope Environment Record(red)
- Line 1 has a variable declaration; add it to the red(global) scope
- Line 7 has a function declaration; add it to the red scope
- Because line 7 is a function declaration, create a new Function Environment Record(green)
- Line 8 has a variable declaration; add it to the green scope
- Line 12 has a function declaration; add it to the red scope
- Because line 12 is a function declaration, create a new Function Environment Record(blue)
- Line 14 has a variable declaration; add it to the blue scope
- Line 17 has a function declaration; add it to the blue scope
- Because line 17 is a function declaration, create a new Function Environment Record(yellow)
- No more formal declarations; move on to the next phase
Imagine the Creation Phase as making an execution plan. It can help us visualize what the execution context will look like. It can even tell us if there is an undeclared keyword.
Execution Phase
Variable creation

- Find helloin the red scope
- Since the variable exists in the red scope, create the variable in the Global(red) Execution Context with the default value undefined
- Assign the value "goodbye"to variablehelloin the red(global) Execution Context
NOTE
Regarding Step 2, setting the variable's default value to undefined is why we will get a functionName is not a function error when we call a function expression before it is declared.
Examples of a function expression below
.js1234567What if there is an undeclared entity?
An error is thrown if a variable, function, class, etc., is missing in the Environment Record.

- Try to find worldzin the Environment Record
- If it cannot be found, throw an error
- Log Uncaught ReferenceError: worldz is not defined
Let's go back to the normal flow.
Invoking the global greet1

- Find greet1in the red scope
- Since the function exists in the red scope, create reference in the Global(red) Execution Context
- greet1is invoked, which will create a green Execution Context
- Find worldin the Environment record
- Since the variable exists in the green scope, create the variable in the green Execution Context with the default value undefined
- Assign the value "WORLD!"to variableworldin the green Execution Context
Invoking console.log inside greet1
It's time to invoke console.log, but where is it coming from?

- Find consolein the green scope
- Since it does not exist in the green scope, go one level up
- Find consolein the red scope
- Luckily, consoleexists in the Global Environment Record, so we can label it as red
- We already have access to the consoleobject in the Global Execution Context.
- Find helloin the green scope
- Since it does not exist in the green scope, go one level up
- Find helloin the red scope
- helloexists in the red scope, and it already has the value of- "goodbye"
- Find worldin the green scope
- worldexists in the green scope, and it already has the value of- "WORLD!"
- After invoking console.logit will log"goodble" "WORLD!"
- End of greet1Execution Context;
I'm now omitting some of the steps for brevity.
Running greet2 and the shadowed greet1

- The function greet2is in the red scope so create a function reference in the Global Execution Context
- Invoke greet2and create a blue Execution Context
- Find greet1in the blue scope
- Since the function exists in the blue scope, create a function reference in the blue Execution Context
- Invoke greet1and create a yellow Execution Context
Invoking console.log inside the shadowed greet1

- Since we did not overwrite console, we already know thatconsoleis in the global scope
- Same as before, the value is resolved by going one step up until we get the matching scope and Execution Context
- hellohas a value of- undefinedbecause it points to the blue Execution Context
- worldhas a value of- "world"because it points to the red Execution Context
- After invoking console.logit will logundefined "world"
- End of the inner greet1Execution Context;
Updating the value of hello inside greet2

- Assign the value "hello"to thehellovariable in blue Execution Context
- Run inner greet1again to create a new yellow Execution Context
Running the shadowed greet1, AGAIN!

- After running greet1the first time, we already have colored what the scopes in line 18 will be.
- The second time we run the function, we already know which box to look at.
- consoleis in red
- hellois in blue
- worldis in red
- So the console will output "hello" "world"
How about let and const?
There is the misconception that we can use let and const to avoid the issue of hoisting. The thing is, let and const both "hoist", but they get an unaccessible reference instead of undefined initially.

- Because we declare worldin line number 22, lines 1-21 are considered to be its TDZ(Temporal Dead Zone)
- In line number 2, we are trying to access the variable world
- Since line number 2 is in the TDZ, we throw Uncaught ReferenceError: Cannot access 'world' before initialization
If you are not convinced, try running the code snippet below.
.js1234567Conclusion
Having a better mental model is essential to be a better JavaScript developer. Instead of blaming the "weird" behavior of JavaScript, we can actually use them to our advantage.