[JS#5 WIL π€ Post]
JavaScript hoisting refers to the process where the compiler allocates memory for variable and function declarations prior to execution of code [1]. That means that declarations are moved to the top of their scope before code execution regardless of whether its scope is global or local.
Table Of Contents
- Javascript Hoisting
- Javascript Context Execution: Creation and Execution Phase
- Default Values:
undefined
andReferenceError
let
hoisting and Temporal Dead Zone (TDZ)- Variable Hoisting
- Function Hoisting
- Conclusion
π Javascript Hoisting
Conceptually speaking,
hoisting is the compiler splitting variable declaration and initialization and moving only the declarations to the top of the code.
Thus, variables can appear in code before they are even defined. However, the variable initialization will only happen until that line of code is executed.
The code snippets below show hoisting in action. The first snippet shows how the code is expected to be written: declare the function first and use/invoke it after.
function printIamHoisted(str) {
console.log(str);
}
printIamHoisted("Am I hoisted??")
On the snippet below, the method is invoked first, and the function declaration is written next. However, both of them have the same output - they print the string Am I hoisted?
printIamHoisted("Am I hoisted?")
function printIamHoisted(str) {
console.log(str);
}
So even though the function is called before it is written, the code still works. This happens because of how context execution works in Javascript.
π Javascript Context Execution
When a Javascript engine executes code, it creates execution context. Each context has two phases: creation and execution.
π Creation Phase
When a script executes, the JS engine creates a Global Execution Context. In this phase, it performs the following tasks:
- Create a global object
window
in the web browser - Create a
this
object binding which pertains to the global object - Setup memory for storing variables and function references
- Store the declarations in memory within the global execution context with
undefined
initial value.
Lookin back at this snippet,
printIamHoisted("I am hoisted!!!")
function printIamHoisted(str) {
console.log(str);
}
the global execution context at this phase, would somehow look like this .
π Execution Phase
At this phase, the JS engine executes the code line by line. But by virtue of hoisting, the function is declared regardless of line order, so there is no problem calling/invoking the method prior the declaration.
For every function call, the JS engine creates a new Function Execution Context. This context is similar to global execution context, but instead of creating the global object, it creates the arguments
object that contains references to all the parameters passed to the function. The context during this phase would look somewhat like :
π Only the declarations (function and variable) are hoisted
JS only hoists declarations, not initializations. If a variable is used but it is only declared and initialized after, the value when it is used will be the default value on initialization.
π Default Values: undefined
and ReferenceError
For variables declared with the var
keyword, the default value would be undefined
.
console.log(hoistedVar); // Returns 'undefined' from hoisted var declaration (not 6)
var hoistedVar; // Declaration
hoistedVar = 78; // Initialization
Logging the hoistedVar
variable before it is initialized would print undefined
. If however, the declaration of the variable is removed, i.e.
console.log(hoistedVar); // Throw ReferenceError Exception
hoistedVar = 78; // Initialization
a ReferenceError
exception would be thrown because no hoisting happened.
π let
hoisting: Temporal Dead Zone (TDZ)
Variables declared with let
and const
are also hoisted. However, unlike variables declared with var
, they are not initialized to a default value of undefined
. Until the line in which they are initialized is executed, any code that access them, will throw an exception. These variables are said to be in a "temporal dead zone" (TDZ) from the start of the block until the initialization has completed. Accessing unintialized let
variables would result to a ReferenceError
.
{ // TDZ starts at beginning of scope
console.log(varVariable); // undefined
console.log(letVariable); // ReferenceError
var varVariable = 1;
let letVariable = 2; // End of TDZ (for letVariable)
}
The term "temporal" is used because the zone depends on the execution order (referring to time - temporal) rather than the order in which the code is written (position). However, the code snippet below will work because even though sampleFunc
uses the letVariable
before it is declared, the function is called outside of the TDZ.
{
// TDZ starts at beginning of scope
const sampleFunc = () => console.log(letVariable); // OK
// Within the TDZ letVariable access throws `ReferenceError`
let letVariable = 97; // End of TDZ (for letVariable)
sampleFunc(); // Called outside TDZ!
}
π Variable Hoisting
Remember that all function and variable declarations are hoisted to the TOP of the scope. Declarations are processed before any code is executed. With this, undeclared variables do not exist until the code assignment is executed. Variable assignment to an undeclared variable implicitly creates it as a global variable when the assignment is executed. That means that any undeclared variable (but assigned) is a global variable.
function demo() {
globalVar = 34;
var functionScopedVar = 78;
}
demo();
console.log(globalVar); // Output: 34
console.log(functionScopedVar) // throws a ReferenceError
This is why it is always good to declare variables regardless of whether they are of function or global scope.
π ES5 Strict Mode
Introduced in EcmaScript 5, strict mode is a way to opt in to a restricted variant of JS. Strict mode make several changes to normal JS semantics
- Eliminates silent errors by throwing them
- Prohibits syntax that might be defined in future version of ES
- Fix mistakes that make JS engines perform optimizations
With regards to hoisting, using strict mode will not tolerate the use of variables before they are declared.
π Function Hoisting
JS functions can be declarations or expressions.
Function declarations are hoisted completely to the top. That is why a function can be invoked even before it is declared.
amIHoisted(); // Output: "Yes I am."
function amIHoisted() {
console.log("Yes I am.")
}
Function expressions, are NOT hoisted. This is because of the precedence order of JS functions and variables. The snippet below would throw a TypeError
because the hoisted variable amIHoisted
is treated as a variable, not a function.
amIHoisted(); //Output: "TypeError: expression is not a function
var amIHoisted = function() {
console.log("No I am not.");
}
The code execution of the code above would somehow look like this
var amIHoisted; // undefined
amIHoisted();
/*Function is invoked, but from the interpreter's perspective it is not a function.
Thus would throw a type error
*/
amIHoisted = function() {
console.log("No I am not.");
}
/*The variable is assigned as a function late. It was already invoked before the assignment.*/
π Hoisting Order of Precedence
- Variable assignment takes precedence over function declaration.
The type of
amIABoolean
would be aboolean
because the variable is assigned to a valuetrue
.
var amIABoolean = true;
function amIABoolean() {
console.log("No.")
}
console.log(typeof amIABoolean); // Output: boolean
- Function declarations take precedent over variable declarations. From the snippet below, the type of
amIAFunction
would be afunction
because on the first line, the variable is only declared, not assigned. Since function declarations takes precedence, it is resolved to typefunction
.
var amIAFunction;
function amIAFunction() {
console.log("Yes.")
}
console.log(typeof amIAFunction); // Output: function
Conclusion
Hoisting in JS is the compiler splitting variable declaration and initialization and moving only the declarations to the top of the code. So even though the functions and variables are called/used before they are written, the code still works. This happens because of how context execution works in Javascript.
Note that only declarations are hoisted, not initializations. For variables declared with the var
keyword, the default value would be undefined
. For let
variables, until the line in which they are initialized is executed, any code that access them, will throw an exception. These variables are said to be in a "temporal dead zone" (TDZ) from the start of the block until the initialization has completed. Accessing unintialized let
variables would result to a ReferenceError
.
That is it for the basics of JS hoisting, my fifth WIL(What I Learned) dev post π.
As always, cheers to lifelong learning π·!