Demystifying Hoisting in JavaScript: Understanding Variable Declarations

Demystifying Hoisting in JavaScript: Understanding Variable Declarations

Prerequisites

It is assumed that the reader of this article has at least a basic knowledge of the following concepts:

  • Function declaration vs function expression

  • Arrow functions

  • Scope and scope chain

What Is Hoisting?

Hoisting is a mechanism in JavaScript that allows variables and function declarations to be moved to the top of their containing scope during the compilation phase before the code is executed.

Before any javascript code runs, it passes through two phases:

  1. Compilation Phase: In this phase, the JavaScript engine analyzes the code and prepares it for execution.

    In this phase, the JavaScript engine also hoists variables and functions to the top of their respective scopes. This makes some variables accessible before their actual declarations.

  2. Execution Phase: Once the compilation phase is complete, the JavaScript engine moves on to the execution phase. In this phase, the code is executed line by line. Here, variables and functions are assigned values and executed.

This implies that even before your code runs, javascript already knows all the variables and functions in the code.

Run the code below on your local machine:

console.log(x)
let x=5

Run this too:

console.log(y)

What do you notice?

You should get two different error messages as shown below:

//ReferenceError: Cannot access 'x' before initialization.
//ReferenceError: x is not defined

What is the difference between these two error messages? What can you infer?

Hoisting Of Variable Declarations: var vs. let and const

When a variable declared with var is hoisted, only the declaration is hoisted to the top of its scope, not the initialization. This means that the variable is created but remains undefined until the point in the code where it is explicitly assigned a value.

Run the code below on your local machine:

console.log(topic)
var topic='hoisting in javaScript'
//undefined

Notice that undefined is printed to the console. This buttresses our point that var variables are hoisted to undefined.

As for let and const, they are both hoisted to uninitialized. If you try to access variables defined with either let or const before initialization, you get a reference error.

This explains the phenomenon noticed in the first example code:

console.log(x)
let x=5
//ReferenceError: Cannot access 'x' before initialization.

The Temporal Dead Zone (TDZ)

The temporal dead zone(TDZ) is a region where a variable cannot be accessed. Accessing a variable in this region causes a reference error.

The TDZ begins at the top of a variable's scope and ends wherever the variable is defined.

{ //TDZ starts here
    //This is also a TDZ 
    //This is also a TDZ 
let x=5 //TDZ ends here
}

Trying to access x anywhere in the TDZ results in a reference error as shown below:


    {//TDZ starts here
       console.log(x) //This is also a TDZ 
        //This is also a TDZ 
        let x=5 //TDZ ends here
}
//ReferenceError: Cannot access 'x' before initialization.

This behavior might make you think that variables declared with let and const are not hoisted. This view is quite inaccurate. All variables are hoisted. Run the code below on your machine:

let fruit='Mango'
{
console.log(fruit)
let fruit='Banana'
}
//ReferenceError: Cannot access 'x' before initialization.

Pause for a moment and think. Why does this show that variables defined with let are hoisted?

Can you figure anything out yet? If not, here's a hint. The answer is related to the scope chain concept.

The first fruit is defined in the global scope, so it is available everywhere. However, when a variable points to different values, the value closer to the scope of the variable takes precedence. Therefore, the value of fruit here is Banana, and not Mango.

If the fruit variable weren't hoisted, the console.log() statement would have printed out Mango. Clearly, that is is not the case, rather, it throws the same error as before.

Therefore, we can know for sure that variables declared with let and const are hoisted.

Hoisting of Functions

There are two ways to define a function in JavaScript; function declaration and function expression.

When a function declaration is hoisted, it is hoisted to the value of the function itself. That is, you can call a function before declaring the function. Consider the code below:

greet('John')
function greet(person){
    console.log('Hello, ' + person)
}
// Hello, John

However, the value of a hoisted function expression depends on what keyword the function was defined with. If a function was defined with let or const, the function will be hoisted to uninitialized, Therefore, calling the function before initialization will result in a reference error.

greet('John')
const greet=function(person){
    console.log('Hello, ' + person)
}
//ReferenceError: Cannot access 'greet' before initialization

The same behavior also holds for functions defined with the let keyword.

What do you think would happen if the function was defined with the var keyword? Will it result in an error or undefined? Think a moment about it, then, swap const for var in the code above on your local machine.

Was your answer correct? If yes, why, and if no, why?

greet('John')
var greet=function(person){
    console.log('Hello, ' + person)
}
//TypeError: greet is not a function

The reason for this is that the function was hoisted to a value of undefined. That is the value of greet is undefined. In JavaScript, calling any data type that is not a function will return a TypeError.

To prove this run the following code on your local machine:

1()
 //TypeError: 1 is not a function
undefined()
//TypeError: undefined is not a function

'hello'()
//TypeError: "hello" is not a function

true()
 //TypeError: true is not a function

Note that calling different data types which are not functions return the same error message. So calling greet is tantamount to calling undefined.

Since arrow functions are always defined using one of the three keywords, it exhibits the same behavior as function expression about hoisting.

The table below provides a summary of the behavior of different data types concerning hoisting.

HoistedValue when called
Variables declared with varYesUndefined
Variables defined with let and constYesUninitialized (Error)
Function declarationYesThe function's value
Function expressionDepends on the variable used to define itDepends on the variable used to define it

Tips for Writing Hoist-Friendly Code

You can adopt certain practices to make your code more hoist-friendly when writing JavaScript code. These tips will help you leverage hoisting effectively and avoid potential issues. Here are some suggestions:

  1. Declare Variables at the Top: To ensure predictable behavior and improve code readability, declare your variables at the top of their respective scopes. This makes it clear which variables are used within the scope and prevents accidental use before the declaration.

  2. Initialize Variables: While variable declarations are hoisted, the initialization remains in place. To avoid confusion and unexpected behavior, initialize variables with appropriate values as close to their declarations.

  3. Avoid Redeclaration: Repeating variable declarations within the same scope can lead to unintended consequences and bugs. Make sure to declare each variable only once within its scope to prevent confusion and maintain code clarity.

  4. Declare Functions Before Use: Although function declarations are hoisted, it's a good practice to declare functions before calling them. This improves code readability and makes it easier to understand the flow of the program.

  5. Use let and const for Block Scoping: Prefer using let and const for variable declarations, as they introduce block scoping. This reduces the risk of variables being accessed before their declaration and helps avoid issues related to the Temporal Dead Zone (TDZ).

  6. Avoid Reliance on Hoisting: While hoisting can be helpful in certain situations, it's generally recommended to write code that does not heavily rely on hoisting. Instead, focus on organizing your code logically and maintaining clear variable and function scopes.

  7. Use Strict Mode: Enable strict mode in your JavaScript code by adding the "use strict"; directive at the beginning of your script or function. Strict mode helps catch certain hoisting-related errors and promotes better coding practices.

  8. Test and Debug: Regularly test your code and pay attention to any unexpected behaviors or errors. Debugging can help identify hoisting-related issues and ensure that your code behaves as expected.

By following these tips, you can write code that takes advantage of hoisting while minimizing potential pitfalls. It promotes code readability, maintainability, and reduces the likelihood of encountering hoisting-related bugs.

Conclusion

In conclusion, understanding and mastering hoisting in JavaScript can greatly contribute to writing cleaner and more reliable code. By grasping the concepts of hoisting, you gain a deeper insight into how JavaScript operates behind the scene. This knowledge empowers you to organize your code more effectively, improve readability, and reduce the chances of encountering hoisting-related bugs.