A Comprehensive Guide on JavaScript Hoisting

A Comprehensive Guide on JavaScript Hoisting

As a JavaScript developer, one major concept you will be exposed to is a seemingly subtle mechanism known as hoisting. As simple and easy as this may sound, it could lead to bugs, and errors, and might become quite confusing, especially for beginners. Hence, in this article, we will explore various types of hoisting, their interaction with scope, as well as the best practices to navigate the various pitfalls that could arise with using hoisting. By the end of this article, you will not only understand hoisting but also appreciate its interaction in shaping robust Javascript code.

What is hoisting?

Hoisting is a term used to describe the behavior of a variable and function being moved to the top of the environment in which they are declared or referenced during the compilation phase.

Why is hoisting important?

Understanding hoisting is important to help us understand how Javascript interpretes code to provide error-free codes, which in turn provides us with more flexibility and increases readability of our codes. The intricacies of hoisting becomes more significant when dealing with variable and function declarations in different scopes. Now, let's dive deeper into the various types of hoisting that we have.

How does hoisting work in JavaScript?

For every JavaScript code that is run, there are two phases involved before the output is delivered; the compilation phase and the execution phase. During the compilation phase, which happens before the execution, all executable codes are run line by line thus allocating a memory location to every variable declaration and function definition; this causes them to be moved up to the top of their containing scope thus giving access to JavaScript to invoke the variables or functions even before they are explicitly declared in the code.

Variable Hoisting

Variable hoisting is the behavior of Javascript where variables are moved to the top of the scope in which they are declared.

console.log(myName); //Output: undefined
var myName = "Lade"; 
console.log(myName); //Output: Lade

From the example above, the output from the first console.log is "undefined" and this is because only variable declarations are hoisted and not the assignments. So, the variable, 'myName' is available and accessible within this scope hence the output but because the initialization had not taken place at this point, 'myName' was initialized with a default value of 'undefined'.

The case is different with the ES6 modification to variable declaration by the introduction of let and const.

'Let' and 'const' variables are hoisted but not initialized, hence, accessing them before the actual declaration results in 'ReferenceError'.

console.log(anime); //Uncaught ReferenceError: Cannot access 'anime' before initialization
let anime = 'Naruto'; 
Console.log(anime);  //Naruto

Function Hoisting

Functions in Javascript are also hoisted, unlike variables where only declarations are hoisted, the entire function including the body is moved to the top during the compilation phase.

Let's consider this simple example:

sayHello(); //Output: Hello! 

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

You will notice that even though we call the sayHello function before its actual declaration, JavaScript doesn't throw an error. Understanding function hoisting is important to write clean and organized code, especially in a situation where function depends on each other.

It is important to note that anonymous functions, function expressions, and arrow functions only have their declarations hoisted and not their body. They follow variable hoisting as seen in the example below:

nonHoistedFunction(); //  Uncaught TypeError: nonHoistedFunction is not a function

var nonHoistedFunction = function (){
  console.log('Not a function'); 
}
nonHoistedAnonymousFunction() //Uncaught TypeError: nonHoistedAnonymousFunction is not a function
var nonHoistedAnonymousFunction = function(){
console.log('Not a function'); 
}
nonHoistedArrowFunction() //Uncaught TypeError: nonHoistedArrowFunction is not a function

var nonHoistedArrowFunction = () => { 
 console.log('Not a Function Also'); 
}

Class Hoisting

Classes in Javascript are hoisted but they are not fully hoisted. When you declare a class with the ‘class’ keyword, the declaration is hoisted but the class itself is not hoisted to the top while the class body and constructor is not initialized until it is encountered in the code.

let person = new Person(); // Uncaught ReferenceError: Cannot access 'Person' before initialization

class Person{
   constructor(name){
     this.name=name;
   }
 }

Scope and Hoisting

Before we go into scope's interaction with hoisting, let's clarify what scope is. Scope is the region or area where your variables or functions are accessible. JavaScript has two main types of scopes namely: global (or window) scope and local scope.

Global scope refers to variables or functions that are accessible throughout your entire program while local scope is only accessible within a specific function.

How then does hoisting interact with different scopes? Hoisting interacts with scopes differently. It is important to consider the scope and hoisting behavior while declaring variables and functions to prevent unintended errors when you write your codes.

Consider the following example:

function outerFunction() {

  console.log(innerVar); //undefined

  if (true) {
    var innerVar = "Inner variable";
  }
console.log(innerVar); //Inner variable
}
outerFunction();

In this function, 'innerVar' is hoisted to the top of the scope. When we logged its value before its declaration, it printed undefined. However, within the if block, the variable was initialized and when we logged it again it displayed the assigned value. This behavior demonstrates the interaction of variable hoisting within nested scope.

This same behavior also exists when we declare variables within the scope of a function.

Example:

function sample(){
 console.log(answer); //undefined
 var answer = 10;
 console.log(answer); //10
}

Within this Sample function, the answer is initialized with the default value of undefined when it was hoisted and remained unassigned until its actual assignment hence the output 10 in the second console.log statement.

This behavior is different with 'let' and 'const' variables, Let's critically examine this example:

function newLetVariable(){
  console.log(answer); //Uncaught ReferenceError: Cannot access 'answer' before initialization
  let answer = "I don't know!"; 
 console.log(answer); //I don't know
 }
 newLetVariable();

You would notice that they had an error as the first output, because, let and const, which have similar hoisting behaviors are hoisted to the top of their actual block scope but remain uninitialized until their actual declaration.

'Let' and 'Const' are also only available within the block scope in which they are declared. Consider this example:

function sample(){
 console.log(myConst); //Uncaught ReferenceError: myConst is not defined
if(true){
 const myConst = 'Inner constant'; 
 console.log(myConst); // Inner constant
 }
console.log(myConst); //Uncaught ReferenceError: myConst is not defined
}

sample();

From this example, you would notice that we have an error as the output for the first console.log statement because the myConst variable was not initialized, unlike the 'var' variables.

Hoisting can help improve code's readability and code organization. However, it could lead to unintentional errors if not properly maximized.

Adhering to the following best practices can help prevent these unintended errors: I). Use 'let' and 'const' instead of var: For variable declarations, use either 'let' or 'const' because they are not hoisted outside of their containing scope, that is, the global scope which in turn helps to forestall unintended behaviors.

//Good 
let play = 'Judo'; 
const height = '25cm'; 

//Avoid
var count = 10;

ii). Declare variables at the top: Explicitly declare and initialize variables at the top of your scope to minimize reliance on hoisting. This also improves code readability and organization.

//Good 
function new() {
let x = 1;
let y = 2;
}

//Avoid
function bad(){
 let x; 
 let y; 

x = 1;
y = 2;
}

iii). Use function declaration to minimize unintended errors

//Good
sayHi(); 
function sayHi(){
 console.log(Hi,  everyone!)
}

// Avoid
sayHi(); 
var sayHi = function(){
console.log('Say Hi!'); 
}

iv). Use linting tools: Using listing tools such as ESLint, will help maintain and enforce coding standards. They also help to maintain a consistent rendering style across browsers.

Conclusion

In this comprehensive guide, we have been able to understand the importance of hoisting in writing clean and easy to maintain code. We have also learnt how we can prevent unintended behaviors caused by hoisting by following some coding best practices.

Thank you for reading through. I hope you have learned as much as I have with this article.

Please leave comments, questions or suggestions. Let's continue to learn together.