JavaScript Hoisting Explained

javascript
Hoisting explained text and a person next to it, trying to lift a heavy object with a stick

What is Hoisting?

Hoisting refers to the specific behavior of the JS interpreter, where variables and function declarations are moved to the top of their containing scope during the compile phase, before the code has been executed.

It's important to note that only the declarations are hoisted not the initializations. This means if a variable is declared and initialized after using it, the variable will be undefined, because only the declaration was hoisted to the top - not the initialization.

 

Variable Hoisting

Let's explore how hoisting works in two scenarios: the traditional way using var and the modern way using ES6 declarations (let and const).

Traditional Hoisting with var

In the good ol’ days, the primary way to declare variables was using the var keyword.

When a variable is declared using var, it gets hoisted to the top of its function or global scope (note that while the declarations are hoisted, the initializations are not. This means if you try to use a variable before it is initialized, you will get a result of undefined.

For example:

console.log(message); // undefined
    var message = "Hello, hoisting!";
    console.log(message); // Hello, hoisting!
    

In the example above, even though I’m trying to log the message variable before its declaration, JavaScript hoists the declaration to the top, making it undefined at the first log statement and then assigning the value later.

⚠️ This behavior can lead to unexpected results and bugs, especially if you're not aware of how hoisting works.

Modern Hoisting with ES6 let and const

The let and const also have a hoisting behavior, but it's slightly different from var.

When a variable is declared using let, it gets hoisted to the top of the block, but it's not initialized. This means that you cannot access the value of a variable until it's reached in the code (you’ll get Reference Error).

The const variable means that it contains a constant value and it can’t be updated / re-declared.

  • const declarations are hoisted too but they are not initialized (just like let).

Unlike var, the let and const declarations have block-level scope.

let cherryStatus = "ripe";
    let quantity = 5;
    
    if (quantity > 3) {
        let message = "pick cherries!";
        console.log(message); // "pick cherries!"
    }
    
    console.log(message); // ReferenceError: message is not defined
    

Here, I am trying to access message outside the block where it was declared. But as a result I’m getting an error due to the block-scoping nature of the let keyword, which prevents the variable from being accessible outside the block in which it’s defined.

 

Another example with let:

console.log(message); // ReferenceError: Cannot access 'message' before initialization
    let message = "Hello, ES6 hoisting!";
    console.log(message); // Hello, ES6 hoisting!
    

In this case, trying to access the message variable before its declaration results in a ReferenceError. The variable is hoisted to the top of its block scope, but it is not initialized until the declaration statement.

Similarly, using const will also result in a ReferenceError if accessed before declaration:

console.log(pi); // ReferenceError: Cannot access 'pi' before initialization
    const pi = 3.14159;
    console.log(pi); // 3.14159

 

💡 When attempting to access a let or const variable before its declaration, a reference error occurs due to the presence of the temporal dead zone (TDZ)

 

The temporal dead zone

The Temporal Dead Zone (TDZ) in JavaScript refers to the period between the entering of scope and the point where a variable is declared. During this temporal dead zone, if you try to access the variable before its declaration, you will get a ReferenceError. This concept is particularly associated with the let and const declarations in ECMAScript 6 (ES6) and later..

 

Here’s an example:

function example() {
      // TDZ for 'a' starts here
      console.log(a); // ReferenceError: Cannot access 'a' before initialization
      // TDZ for 'a' ends here
      let a = 42;
      console.log(a); // 42
    
      if (true) {
        // TDZ for 'b' starts here
        console.log(b); // ReferenceError: Cannot access 'b' before initialization
        // TDZ for 'b' ends here
        let b = 7;
        console.log(b); // 7
      }
    
      // TDZ for 'c' starts here
      console.log(c); // ReferenceError: Cannot access 'c' before initialization
      // TDZ for 'c' ends here
      const c = 99;
      console.log(c); // 99
    }
    
    example();
    

Key Differences and Best Practices

  • Scope: var has function-level, let and const have block-level scope, reducing unintended side effects.
  • Hoisting: var hoists the entire declaration, often causing undefined initial value. let and const hoist only declaration, accessing them before results in ReferenceError.
  • Reassignment: const can't be reassigned, enhancing predictability and maintainability.

 

Modern declarations (let and const) mitigate traditional var hoisting issues and align with best practices.

 

Function Hoisting

Function declarations are also hoisted in JavaScript, which means you can use a function before it's declared in your code.

This is different from variable hoisting, because for functions, both the declaration and the function body are hoisted. Here's an example:

console.log(myFunction()); // output: "Hello, World!"
    function myFunction() {
      return "Hello, World!";
    }
    
    

In this case - myFunction() is called before it's declared, but the code still works because of function hoisting.

Just like with variables, the function declaration - myFunction() is hoisted to the top, allowing us to call the function before its actual declaration.

Behind the scenes, the code is interpreted like this:

function sayHello() {
      console.log("Hello, hoisting!");
    }
    
    sayHello(); // Output: Hello, hoisting!
    
    

Conclusion

In conclusion, JavaScript hoisting is a unique feature that can cause some unexpected results if not fully understood. By being aware of how JavaScript hoists variables and functions, you can write cleaner and more predictable code. The key takeaways are that var and function declarations are hoisted to the top of their scope and let and const are hoisted to the top of their block but are not initialized until their declaration is encountered in the code.