My Logo

PUBLISHED JUNE 10, 2024

Teghen Donald Ticha
Lastly Updated: 3 months ago
Reading time 9 mins

What is Hoisting in JavaScript?

Discover what hoisting is in JavaScript, how it affects variables and functions, and best practices to avoid common pitfalls. Learn also about Temporal Dead Zone (TDZ).
What is Hoisting in JavaScript?

Prerequisite

Before diving into hoisting in JavaScript, it's beneficial to have a basic understanding of the following concepts:

  1. 1.JavaScript Syntax and Basics: Familiarity with basic JavaScript syntax, including variables, functions, and control structures.
  2. 2.JavaScript Execution Context: Understanding what an execution context is and how JavaScript code is executed in different environments (e.g., browser, Node.js).
  3. 3.Variables and Data Types: Knowledge of different types of variables (var, let, const) and basic data types in JavaScript.
  4. 4.Functions: Basic understanding of how to declare and invoke functions in JavaScript, including function declarations, function expressions, and arrow functions.

If you’re new to these concepts, I recommend that you review them first, to fully grasp the details discussed in this guide.

JavaScript, a dynamic scripting language, operates within an execution context, defining the environment in which code is executed.

This context encompasses the `Variable Object`, `Scope Chain`, and `this` keyword.

  • Variable Object (VO): Contains variable declarations, function declarations, and function arguments.
  • Scope Chain: Determines the accessibility of variables by referencing the Variable Object and the outer environment.
  • this Keyword: Refers to the object that is currently executing the code, facilitating access to object properties and methods.


Understanding the execution context is fundamental for JavaScript developers as it governs how variables and functions are accessed and manipulated during program execution.

Definition of Hoisting

Hoisting is a fundamental mechanism in JavaScript that involves the automatic lifting of variable and function declarations to the top of their containing scope during the `compilation phase`.

This behavior allows variables and functions to be accessed and utilized before their formal declarations within the code.

Importance of Understanding Hoisting

Hoisting is a crucial concept in JavaScript that affects how variable and function declarations are interpreted during code execution.

Understanding hoisting provides insight into how JavaScript code behaves and aids in writing cleaner, more organized code.

It helps prevent unexpected behavior and enhances code readability and maintainability.

How Hoisting Works in JavaScript

During the compilation phase of JavaScript code execution, the JavaScript engine scans through the code to identify variable and function declarations.

It then allocates memory for these declarations, regardless of their actual placement within the code.

  • Variable Hoisting: Variables declared using the var keyword are hoisted to the top of their scope.
    However, only the declaration, not the initialization, is hoisted.
    This means that while you can access a variable before it's declared, its value will be undefined until the actual initialization is encountered in the code.
console.log(myVar); // Output: undefined
var myVar = 5;
console.log(myVar); // Output: 5

In the above example, myVar is declared using var, so its declaration is hoisted to the top of the scope. However, its value is undefined until the assignment myVar = 5 is encountered.

  • Function Hoisting: Function declarations are hoisted in their entirety, including both the name and the function body.
    This allows functions to be invoked before their formal declarations within the code.
morningFn(); // outputs : Greeting to all!, The time is : 05:09 (Remember, the time value will be different in your case 😅)

function morningFn () {
    const [h, m] = (new Date()).toISOString().split('T')[1].split(':');
    console.log(`${greeter()}, The time is : ${h}:${m}`);
}

function greeter () {
  return "Greeting to all!";
}

Breakdown: Here, the functions morningFn and greeter is invoked before their formal declaration within the code.

This is possible due to function hoisting, where the entire function declaration is moved to the top of its scope during compilation.

1. Effect on Variables
var Keyword

Variables declared with the var keyword are hoisted to the top of their scope, but their initializations remain in place.

This can lead to scenarios where variables are accessible before they are assigned a value, resulting in unexpected behavior.

console.log(myVarible); // Output: undefined
var myVarible = 10;
console.log(myVarible); // Output: 10

Breakdown: myVariable is declared using var, so its declaration is hoisted to the top of the scope. However, the initialization (myVariable = 10) remains in place.

Therefore, when console.log(myVar) is first called, myVariable is undefined.


Let's have an example with function scope.

// Example with function scope
function fn() {
  console.log(innerVar); // Output: undefined
  var innerVar = "Inside function";
  console.log(innerVar); // Output: Inside function
}
fn();
console.log(innerVar); // ReferenceError: innerVar is not defined

Breakdown: Here, innerVar is hoisted to the top of the function scope. Therefore, it is accessible throughout the function, even before its declaration.

However, attempting to access innerVar outside the function scope results in a ReferenceError.


let and const Keywords

Variables declared with let and const exhibit a different behavior known as the `Temporal Dead Zone (TDZ)`.

In this zone, variables exist but cannot be accessed before their declaration due to how JavaScript's scoping rules work.

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

Breakdown: In this example, attempting to access x before its declaration results in a ReferenceError due to the `TDZ`.

Only after the variable x is declared and initialized can it be accessed and used within the code.


// Example with block scope
if (true) {
    console.log(y); // ReferenceError: Cannot access 'y' before initialization
    let y = "Inside block";
    console.log(y); // Output: Inside block
}
console.log(y); // ReferenceError: y is not defined

Similarly, variables declared with let exhibit `block scope`, meaning they are only accessible within the block they are declared in.

Attempting to access y outside the block where it's declared results in a ReferenceError.

const PI = 3.14;
console.log(PI); // Output: 3.14
PI = 3.14159; // TypeError: Assignment to constant variable

Here, variables declared with const are hoisted but cannot be reassigned after their initialization.

Attempting to reassign a const variable will result in a TypeError.


2. Affect on functions
Function Declarations

Function declarations are hoisted in their entirety, meaning both the name and the function body are moved to the top of their containing scope during the compilation phase.

This allows you to call a function before its formal declaration within the code.

sayHello(); // Output: Hello!

function sayHello() {
    console.log("Hello!");
}

In this example, the function sayHello is invoked before its formal declaration within the code.

This is possible due to function hoisting, where the entire function declaration is moved to the top of its scope during compilation.



Let's have another example with function scope:

// Example with function scope
function fn() {
    greet(); // Output: "Hello from greet!"
    function greet() {
        console.log("Hello from greet!");
    }
}
fn();

Here, the greet function is declared inside the fn function, but it is still accessible within the entire fn function scope due to hoisting.

This illustrates how function declarations are hoisted within their containing scope, allowing them to be invoked before their formal declaration.


Function Expressions

Function expressions, on the other hand, are not hoisted in the same way as function declarations.

Only the variable declaration is hoisted, not the function initialization.

Therefore, attempting to invoke the function expression before its assignment will result in an error.

sayHi(); // TypeError: sayHi is not a function

var sayHi = function() {
    console.log("Hi!");
};

In this example, the variable sayHi is hoisted to the top of its scope, but its initialization remains in place.

Therefore, attempting to invoke sayHi before its assignment results in a TypeError.


Let's have an example with block scope:

if (true) {
  function greet() {
      console.log("Hello from greet!");
  };
  greet(); // Output: "Hello from greet!"
}
greet(); // ReferenceError: greet is not defined

Here, the function expression greet is declared within the block scope of the if statement.

It can only be accessed and invoked within that block scope, demonstrating how function expressions are affected by hoisting and block scope.

As we already know, if we try the samething with a function declaration, the second greet() invocation runs successfully.

if (true) {
  function greet() {
      console.log("Hello from greet!");
  };
  greet(); // Output: "Hello from greet!"
}
greet(); // Output: "Hello from greet!"


Arrow Functions

Arrow functions, introduced in ES6, behave similarly to function expressions in terms of hoisting.

Only the variable declaration is hoisted, not the function initialization.

greet(); // TypeError: greet is not a function

var greet = () => {
    console.log("Greetings!");
};

In this example, attempting to invoke greet before its assignment results in a TypeError.

Arrow functions, like function expressions, are not hoisted in the same way as function declarations.


// Example with object methods
const person = {
    name: "Yohan",
    greet: () => {
        console.log(`Hello, my name is ${this.name}`);
    }
};
person.greet(); // Output: "Hello, my name is undefined"

Here, the arrow function greet is declared within the object person.

However, since arrow functions do not have their own this context, this refers to the global object (window in the `browser`, global in `Node.js`), resulting in undefined.

Best Practices to Avoid Hoisting Issues
1. Declaring Variables and Functions at the Top

To mitigate hoisting-related issues and enhance code clarity, it's advisable to declare variables and functions at the beginning of their respective scopes.

This practice ensures that dependencies are clearly defined and accessible throughout the scope.

let result;
const shouldRunOdd = Math.random() > 0.5;

function processData(type = 'ODD', size = 20) {
  const arr = [];
  for (let i = 1; i < size; i++) {
    arr.push(i);
  }
  return type === 'ODD' ? arr.filter(v => v % 2 !== 0) : arr.filter(v => v % 2 === 0);
}

if (Math.random() > 0.5) {
  result = processData('ODD', 40);
  
} else {
  result = processData('EVEN', 40);
}
console.log(result);

By placing variable and function declarations at the top, developers establish a clear flow of logic and make it easier for others to understand the code structure.


2. Using let and const Over var

Prefer using let and const over var for variable declarations, as they provide block scope and prevent hoisting-related issues.

Additionally, const ensures that variables are not reassigned, promoting immutability and enhancing code predictability.

let counter = 0;
const MAX_COUNT = 10;

while (counter < MAX_COUNT) {
    // Perform operations
    counter++;
}

By leveraging let and const, developers enforce stricter variable scoping rules, reducing the likelihood of unintended side effects and enhancing code maintainability.


3. Understanding the Temporal Dead Zone (TDZ)

Be mindful of the Temporal Dead Zone (TDZ) when working with let and const declarations.

The TDZ refers to the period between a variable's creation and its initialization, during which accessing the variable results in a ReferenceError.

let city = "Douala";

if (true) {
    // The TDZ starts here
    console.log(city); // ReferenceError: Cannot access 'city' before initialization
    let city = "Kigali"; // Variable declaration within the block scope
    console.log(city); // Output: "LKigali"
}

In this example, city is declared outside the block scope and initialized with the value `Douala`.

Inside the if block, another variable city is declared using let, creating a `TDZ` for city within that block.

Attempting to access city before its declaration in the block results in a ReferenceError.

After its declaration, city is accessible and holds the value `Kigali` within the block scope. (Comment the first console.log(city) statement and run the code).



Conclusion

In this article, we've explored the concept of hoisting in JavaScript, which is a crucial aspect of how JavaScript code is executed. Here are the key takeaways:

  • Hoisting is a JavaScript mechanism where variable and function declarations are moved to the top of their containing scope during the compilation phase.
  • Variable declarations using var are hoisted to the top of their scope, while their assignments remain in place. Function declarations are hoisted in their entirety.
  • With the introduction of let and const, block-scoped variable declarations provide more predictable behavior compared to var.
  • Arrow functions and function expressions are not hoisted in the same way as function declarations.
  • The Temporal Dead Zone (TDZ) is the period between the creation and initialization of a variable declared with let and const, during which accessing the variable results in a ReferenceError.

Importance of Understanding Hoisting for Debugging and Writing Clean Code

Understanding hoisting is essential for writing clean, predictable, and maintainable JavaScript code. By grasping how hoisting works, developers can:

  • Debug effectively: Recognizing hoisting behaviors can aid in troubleshooting code errors, such as accessing variables before their declaration or understanding unexpected behavior due to hoisted function declarations.
  • Write cleaner code: Following best practices to mitigate hoisting-related issues, such as declaring variables and functions at the top of their scope and using let and const over var, leads to cleaner and more readable codebases.
  • Enhance code predictability: By understanding hoisting behaviors, developers can anticipate how their code will be executed and avoid common pitfalls associated with variable scoping and function declaration.

In conclusion, a solid understanding of hoisting is fundamental for JavaScript developers to write efficient, bug-free code and maintain high standards of code quality and readability.

Interested in exploring more 🤓? Check out these Related Posts.