The term Hoisting is used in a lot of JavaScript blogs to explain identifier resolution. Using the literal meaning of the word as a metaphor, hoisting is employed to explain how Variables and Function declarations are ‘lifted’ to the top of a function or a global scope.
While this does provide a basic understanding of how JavaScript scoping works, a deeper dive helps to build a stronger foundation.
To understand the fundamentals better, let’s recap on what exactly hoisting means. Also, as a reminder, JS is an interpreted language which means that unlike compiled languages JS code runs line by line.
Consider the following example.
1console.log(notyetdeclared);2// prints out 'undefined'34var notyetdeclared = 'now it is declared';56hoisting();78function hoisting(){9 console.log(notyetdeclared);10 // prints out 'undefined'1112 var notyetdeclared = 'declared differently';1314 console.log(notyetdeclared);15 // prints out 'declared differently'16}
After overanalyzing the code sample above, there are several questions that jump up.
- How is the function being accessed before declaration on line 6?
- How did line 1 not throw an error since at this point no variable called
notyetdeclared
exists? - Why is line 9 printing
undefined
whennotyetdeclared
has already been declared in the global scope?
Javascript is extremely logical and all of this weirdness has a clear explanation.
Starting from the very top, when code is executed in Javascript, an execution context is set up. There are two major types of execution contexts in JS — Global execution context and Function execution context. Since Javascript is based on a single threaded execution model, only one piece of code can be executed at a time. For our code, the process is as follows :
This process is self explanatory but doesn’t really explain the anomalies we
saw during the execution of the code sample. While the execution context keeps
track of the execution of the code, Lexical environment keeps track of the
mapping of identifiers to specific variables. Lexical environment basically is
an internal implementation of the JavaScript scoping mechanism. Generally, the
lexical environment is associated with a specific structure of the JavaScript
code for example a function or a block of code like a for
loop. Whenever a
function is created, a reference to the lexical environment in which it was
created is passed in an internal property named [[Environment]]
.
Covered by all this jargon is a simple and an extremely logical concept. Lets break it down. Lexical environment is a fancy name for something that keeps track of variables and functions within a block of code. In addition to keeping track of local variables, function declarations and parameters, each lexical environment keeps track of its parent lexical environment. So the Javascript Engine’s resolution of the above code sample would look something like this:
To resolve an identifier within a lexical environment, the JS engine checks
the current environment for a reference. If no reference is found, it moves on
to the outer environment by using [[Environment]]
. This goes on until either
the identifier is resolved or a ‘not defined’ error is thrown.
Basically, the execution of JS code happens in two phases. The first phase registers all the variables and function declarations within the current lexical environment. After that is done, the second phase — Javascript execution begins!
So to elaborate on the first phase: it works in two steps.
-
The current code is ‘scanned’ for function declarations. Function expressions and arrow functions are skipped. For every function that is discovered a new function is created and bound to the environment with the function’s name. If the identifier name already exists, its value is overwritten.
-
Then the current environment is scanned for variables. Variables defined with
var
and placed outside other functions are found and an identifier is registered with its value initialized toundefined
. If an identifier exists, the value is left untouched.
Note : let
and const
are block variables and have a slightly different
treatment than var
. More on that in another article.
Now that you have a basic idea of what a lexical environment is, lets get back to our sample code and deal with the questions.
When the global context is set up, the environment is scanned and the
hoisting()
function is attached to an identifier. Then in the next step, the
variable notyetdeclared
is registered and its value is initialized to
undefined
per the steps.
How is the function being accessed before declaration on line 6?
The hoisting()
function is already registered to an identifier in phase 1 and
when the JS code starts executing in the global context in phase 2, it looks up
the lexical environment for hoisting
and finds the function even before its
definition.
How did line 1 not throw an error since at this point no variable
called notyetdeclared
exists?
Similarly, notyetdeclared
is registered to an identifier and initialized to
undefined
in phase 1 and hence no error is thrown.
Finally,
Why is line 9 printing undefined
when notyetdeclared
has already been
declared in the global scope?
Now we are in the hoisting
environment. In phase 1, notyetdeclared
is
registered and initialized to undefined
because in this lexical environment
the variable notyetdeclared
has not been registered yet. If line 12 did
not contain the var
keyword, this would have been a different story.
Hopefully it is clear now that hoisting in JS is a simplistic view and technically the functions and variables aren’t moved anywhere.