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
.js12345
JavaScript will rearrange the code for us to this one.
.js12345
Or, with a slightly more complex example
.js12345678910111213141516171819202122
JavaScript will magically rearrange the variables and function declarations; then, it will run from top to bottom.
.js12345678910111213141516171819202122232425
That'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.
.js12345678910111213141516171819202122
Output
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
.js1234567
What 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 ofundefinedbecause it points to the blue Execution Contextworldhas 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 redhellois in blueworldis 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.
.js1234567
Conclusion
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.