[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

:pushpin: 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.

:pushpin: Javascript Context Execution

When a Javascript engine executes code, it creates execution context. Each context has two phases: creation and execution.

:pushpin: 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 Creation Phase.

:pushpin: 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 : Execution Phase

:pushpin: 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.

:pushpin: 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.

:pushpin:

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!
}

:pushpin: 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.

:pushpin: 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

  1. Eliminates silent errors by throwing them
  2. Prohibits syntax that might be defined in future version of ES
  3. 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.

:pushpin: 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.*/

:pushpin: Hoisting Order of Precedence

  • Variable assignment takes precedence over function declaration. The type of amIABoolean would be a boolean because the variable is assigned to a value true .
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 a function because on the first line, the variable is only declared, not assigned. Since function declarations takes precedence, it is resolved to type function .
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 :smile:.

As always, cheers to lifelong learning :wine_glass:!

[REFERENCES]

  1. MDN Web Docs : Hoisting
  2. Understanding Hoisting in Javascript
  3. Javascript Context Execution
  4. Temporal Dead Zone
  5. Strict Mode

This post is also available on DEV.