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
let
andconst
will 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
hello
in 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 variablehello
in 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
worldz
in 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
greet1
in the red scope - Since the function exists in the red scope, create reference in the Global(red) Execution Context
greet1
is invoked, which will create a green Execution Context- Find
world
in 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 variableworld
in the green Execution Context
Invoking console.log
inside greet1
It's time to invoke console.log
, but where is it coming from?
- Find
console
in the green scope - Since it does not exist in the green scope, go one level up
- Find
console
in the red scope - Luckily,
console
exists in the Global Environment Record, so we can label it as red - We already have access to the
console
object in the Global Execution Context. - Find
hello
in the green scope - Since it does not exist in the green scope, go one level up
- Find
hello
in the red scope hello
exists in the red scope, and it already has the value of"goodbye"
- Find
world
in the green scope world
exists in the green scope, and it already has the value of"WORLD!"
- After invoking
console.log
it will log"goodble" "WORLD!"
- End of
greet1
Execution Context;
I'm now omitting some of the steps for brevity.
Running greet2
and the shadowed greet1
- The function
greet2
is in the red scope so create a function reference in the Global Execution Context - Invoke
greet2
and create a blue Execution Context - Find
greet1
in the blue scope - Since the function exists in the blue scope, create a function reference in the blue Execution Context
- Invoke
greet1
and create a yellow Execution Context
Invoking console.log
inside the shadowed greet1
- Since we did not overwrite
console
, we already know thatconsole
is 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
hello
has a value ofundefined
because it points to the blue Execution Contextworld
has a value of"world"
because it points to the red Execution Context- After invoking
console.log
it will logundefined "world"
- End of the inner
greet1
Execution Context;
Updating the value of hello
inside greet2
- Assign the value
"hello"
to thehello
variable in blue Execution Context - Run inner
greet1
again to create a new yellow Execution Context
Running the shadowed greet1
, AGAIN!
- After running
greet1
the 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.
console
is in redhello
is in blueworld
is 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
world
in 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.