Demystifying Hoisting in JavaScript

Demystifying Hoisting in JavaScript

Let's dive into some intriguing examples to unravel the mysteries of hoisting.

Today, let's dive into the fascinating world of JavaScript and unravel the mystery behind a concept called hoisting.

You might have come across situations where you can use a function or a variable even before they are declared. Well, that's hoisting for you!

Let's start with a simple example:

console.log(movie); // Output: undefined
console.log(getDirector()); // Output: Christopher Nolan

var movie = "Inception";

function getDirector() {
    return "Christopher Nolan";
}

console.log(movie); // Output: Inception
console.log(getDirector()); // Output: Christopher Nolan

In this snippet, we declare a variable movie and a function getDirector but use them before their actual declarations. Surprisingly, JavaScript doesn't throw an error, and we get the expected output.

The Basics of Hoisting

Now, let's explore the concept of hoisting with another example:

console.log("Initial x:", x); // Output: Initial x: undefined

if (true) {
    console.log("Inside if, y:", y); // Output: ReferenceError: Cannot access 'y' before initialization
    var x = 10;
    let y = 20;
    console.log("Inside if, x:", x); // Output: Inside if, x: 10
    console.log("Inside if, y:", y); // Output: Inside if, y: 20
}

console.log("Outside if, x:", x); // Output: Outside if, x: 10
console.log("Outside if, y:", y); // Output: Uncaught ReferenceError: y is not defined

Here, the output might surprise you.

Let's break down this code and understand the complex interplay of hoisting:

  1. Initialconsole.log: We start by logging the initial value of x. Surprisingly, it prints undefined, showcasing the hoisting mechanism in action.

  2. Inside theifblock: Here's where it gets interesting. We attempt to log the value of y before its declaration. With var x = 10;, the variable x is hoisted, but it's initialized to undefined before the assignment. However, when we try to do the same with let y = 20;, JavaScript throws a ReferenceError. Why? Because letdeclarations are not hoisted to the entire block; they are only hoisted within the scope they are declared.

  3. Logging inside theifblock: The subsequent logs demonstrate that despite the attempted log of y causing an error, x is accessible and holds the value assigned within the if block.

  4. Outside theifblock: Moving outside the if block, we log the values of both x and y. x retains its value of 10 due to hoisting, while attempting to access y results in a ReferenceError.

This is due to hoisting, where variable and function declarations are moved to the top of their containing scope during the compilation phase.

Note: Execution contexts in JavaScript are created when functions are invoked, not for every block of code like if statements.

The complexity arises from the combination of var and let declarations within conditional blocks. While var is hoisted to the entire scope, let is confined to the block in which it is declared.

However, things can get a bit tricky:

console.log("Before declaration - x:", x); // Output: Before declaration - x: undefined

if (true) {
    console.log("Inside if - x:", x); // Output: Inside if - x: undefined
    var x = 42;
    console.log("Inside if, after assignment - x:", x); // Output: Inside if, after assignment - x: 42
}

console.log("Outside if - x:", x); // Output: Outside if - x: 42
console.log("Outside if, before declaration - y:", y); // Output: Outside if, before declaration - y: Uncaught ReferenceError: y is not defined

let y = 10;

console.log("Outside if, after declaration - y:", y); // Output: Outside if, after declaration - y: 10

In this case,

  1. Before declaration ofx: Attempting to log the value of x before its declaration, we get undefined. This showcases the hoisting behavior of variables declared with var, which are hoisted but initialized to undefined.

  2. Inside theifblock: Even within the conditional block, attempting to log x before its assignment results in undefined. The subsequent log, after the assignment var x = 42;, reveals the assigned value, showcasing the distinction between declaration and assignment during hoisting.

  3. Outside theifblock: Once outside the block, we log x again, and this time it retains the assigned value of 42. The attempt to log y before its declaration throws a ReferenceError, highlighting the different behavior of let declarations.

  4. After the declaration ofy: Finally, after declaring y with let, logging its value now yields the assigned value of 10.

The trickiness arises from the interplay between hoisting and the nature of variable declarations in JavaScript:

  • Undefined withvar: Variables declared with var are hoisted to the top of their scope but initialized to undefined until their actual assignment in the code.

  • Not Defined withlet: Variables declared with let are hoisted but remain in a "temporal dead zone" until their declaration is reached in the code. Attempting to access them before declaration results in a ReferenceError.

Hoisting with Functions

Now, let's consider a scenario where we print the function itself:

console.log("Before function declaration - magicFunction:", magicFunction); 
// Output: Before function declaration - magicFunction: ƒ magicFunction() {
    console.log("Magic in action!");
}

magicFunction(); 
// Output: Magic in action!

function magicFunction() {
    console.log("Magic in action!");
}

console.log("After function declaration - magicFunction:", magicFunction); 
// Output: After function declaration - magicFunction: function magicFunction() { console.log("Magic in action!"); }

Let's break down this:

  1. Before function declaration: Attempting to log the function magicFunction before its declaration, we get the entire function. This illustrates the hoisting of function declarations, where the function is hoisted to the top of the scope but not yet defined.

  2. Function invocation: Surprisingly, invoking magicFunction before its actual declaration doesn't result in an error. Instead, it gracefully executes the function and logs "Magic in action!".

  3. After function declaration: Logging magicFunction after its declaration reveals the full function definition. This showcases JavaScript's unique handling of function hoisting, allowing the function to be accessed before its declaration in the code.

In other languages, this would typically result in an error, but JS handles it gracefully.

Arrow Functions (with var) and Hoisting

Let's throw a curveball with arrow functions (with var declaration):

console.log("Before arrow function declaration - arrowFunction:", arrowFunction);
// Output: Before arrow function declaration - arrowFunction: undefined

arrowFunction(); 
// Output: Uncaught TypeError: arrowFunction is not a function

var arrowFunction = () => {
    console.log("Arrow function magic!");
};

console.log("After arrow function declaration - arrowFunction:", arrowFunction);
// Output: After arrow function declaration - arrowFunction: () => { console.log("Arrow function magic!"); }

Arrow functions, behaving like normal variables, don't support hoisting.

  1. Before arrow function declaration: Attempting to log the arrow function arrowFunction before its declaration yields undefined. This is in stark contrast to function declarations, showcasing the different hoisting behavior of arrow functions.

  2. Arrow function invocation: Invoking arrowFunction before its actual declaration results in a TypeError. Unlike traditional function declarations, arrow functions do not support hoisting in the same way, leading to this runtime error.

  3. After arrow function declaration: Logging arrowFunction after its declaration reveals the full function definition. While the variable declaration is hoisted, the arrow function itself is not fully hoisted, and thus, attempting to invoke it before declaration results in an error.

Arrow Functions (with let) and Hoisting

If arrowFunction is declared with let instead of var, the behavior will change due to the differences in how let and var handle hoisting and variable initialization. Here's how the modified code would behave:

console.log("Before arrow function declaration - arrowFunction:", arrowFunction);
// Output: Before arrow function declaration - arrowFunction: Uncaught ReferenceError: Cannot access 'arrowFunction' before initialization

arrowFunction(); 
// Output: Uncaught TypeError: arrowFunction is not a function

let arrowFunction = () => {
    console.log("Arrow function magic!");
};

console.log("After arrow function declaration - arrowFunction:", arrowFunction);
// Output: After arrow function declaration - arrowFunction: () => { console.log("Arrow function magic!"); }
  1. Before arrow function declaration: Attempting to log arrowFunction before its declaration with let will result in a ReferenceError rather than undefined. This is because variables declared with let are hoisted to the top of their block (or the top of the global scope if declared outside any block), but they are in a "temporal dead zone" until the line of the declaration is reached. Accessing the variable in this zone throws a ReferenceError.

  2. Arrow function invocation: Invoking arrowFunction before its actual declaration still results in a TypeError. The behavior is the same as when using var.

  3. After arrow function declaration: Logging arrowFunction after its declaration will display the full function definition, just as in the previous example.

Function Expressions and Hoisting

Now, let's explore function expressions:

console.log("Before function expression declaration - getReview:", getReview); 
// Output: Before function expression declaration - getReview: undefined

getReview(); 
// Output: Uncaught TypeError: getReview is not a function

var getReview = function () {
    console.log("A cinematic masterpiece!");
    return "A cinematic masterpiece!";
};

console.log("After function expression declaration - getReview:", getReview); 
// Output: After function expression declaration - getReview: function () { console.log("A cinematic masterpiece!"); return "A cinematic masterpiece!"; }

console.log(getReview()); 
// Output: A cinematic masterpiece!

Surprise! Unlike function declarations, function expressions are not hoisted as-is.

  1. Before function expression declaration: Attempting to log the function expression getReview before its declaration yields undefined. This starkly contrasts with function declarations, emphasizing the unique hoisting behavior of function expressions.

  2. Function expression invocation: Invoking getReview before its declaration results in a TypeError. Unlike function declarations, function expressions are not fully hoisted, leading to this runtime error.

  3. After function expression declaration: Logging getReview after its declaration reveals the full function expression definition. The variable getReview is hoisted, but the function expression itself is not hoisted as a fully defined function.

  4. Function expression invocation after declaration: Invoking getReview after its declaration works as expected, showcasing that the function expression is now fully initialized.

This example solves the puzzle of hoisting and function expressions:

  • Variable Declaration Hoisted: Similar to variables declared with var, the declaration of the variable getReview is hoisted to the top of the scope.

  • Function Expression Limitation: While the variable is hoisted, the function expression itself is not fully hoisted. Attempting to invoke it before its declaration results in a TypeError.

  • Temporal Dead Zone: Function expressions have a "temporal dead zone" during which they are not fully initialized, causing runtime errors when accessed before their declaration.

In Summary

  1. Variables are Hoisted withundefined: Variable declarations get hoisted with an initial value of undefined during the memory creation phase.

  2. Function Declarations vs. Expressions: Function declarations are fully hoisted, while function expressions are hoisted but not initialized.

  3. Arrow Functions Play it Cool: Arrow functions don't fully embrace hoisting like traditional function declarations. Arrow functions, behaving like variables, get hoisted and initialized as undefined.

  4. Hoisting is a mechanism in JS where variable and function declarations are moved to the top of their scope before execution.

  5. UNDEFINED means the variable has been declared but not assigned a value, while NOT DEFINED means the variable is not declared.

  6. Mind the Order: The order of declarations matters, especially when using function expressions.

So there you have it, a peek into the magical world of hoisting in JavaScript.

Thank you!

Did you find this article valuable?

Support Prerana Nawar by becoming a sponsor. Any amount is appreciated!