Everything about Hoisting in JavaScript

Everything about Hoisting in JavaScript

Hoisting || Interview Questions || Logical questions

Hoisting :

In JavaScript, Hoisting is the default behavior of moving all the declarations at the top of the scope before code execution. Basically, It gives us an advantage that no matter where functions and variables are declared, they are moved at the top of their scope regardless of the whether their scope is global or local.

  • A Strict Definition of Hoisting Suggests that variable and function declarations are physically moved to the top of your code, but this is not in fact what happens. Instead, the variable and function declarations are put into memory during the compile phase but stay exactly where you typed them. *

Note: JavaScript only hoists declarations, not the initializations.

JavaScript allocates memory for all variables and functions defined in the program before execution.

Let us understand what exactly this is: The following is the sequence in which variable declaration and initialization occur.

Declaration –> Initialisation/Assignment –> Usage

// Variable lifecycle
let a;        // Declaration
a = 100;      // Assignment
console.log(a);  // Usage

However, since JavaScript allows us to both declare and initialize our variables simultaneously, this is the most used pattern:

 let a = 100;

Note: Always remember that in the background the Javascript is first declaring the variable and then initializing them. It is also good to know that variable declarations are processed before any code is executed.

However, in javascript, undeclared variables do not exist until code assigning them is executed. Therefore, assigning a value to an undeclared variable implicitly creates it as a global variable when the assignment is executed. This means that all undeclared variables are global variables.

Variable hoisting with var

When the interpreter hoists a variable declared with var, it initializes its value to undefined. The first line of code below will output undefined:

console.log(foo); // undefined

var foo = 'bar';

console.log(foo); // "bar"

As we defined earlier, hoisting comes from the interpreter splitting variable declaration and assignment. We can achieve this same behavior manually by splitting the declaration and assignment into two steps:

var foo;

console.log(foo); // undefined

foo = 'foo';

console.log(foo); // "foo"

Remember that the first console.log(foo) outputs undefined because foo is hoisted and given a default value (not because the variable is never declared). Using an undeclared variable will throw a ReferenceError instead:

console.log(foo); // Uncaught ReferenceError: foo is not defined

Using an undeclared variable before its assignment will also throw a ReferenceError because no declaration was hoisted:

console.log(foo); // Uncaught ReferenceError: foo is not defined
foo = 'foo';      // Assigning a variable that's not declared is valid

By now, you may be thinking, "Huh, it's kind of weird that JavaScript lets us access variables before they're declared." This behavior is an unusual part of JavaScript and can lead to errors. Using a variable before its declaration is usually not desirable.

Thankfully the let and const variables, introduced in ECMAScript 2015, behave differently.

Variable hoisting with let and const

Variables declared with let and const are hoisted but not initialized with a default value. Accessing a let or const variable before it's declared will result in a ReferenceError:

console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization

let foo = 'bar';  // Same behavior for variables declared with const

Notice that the interpreter still hoists foo: the error message tells us the variable is initialized somewhere.

The temporal dead zone

The reason that we get a reference error when we try to access a let or const variable before its declaration is because of the temporal dead zone (TDZ).

The TDZ starts at the beginning of the variable's enclosing scope and ends when it is declared. Accessing the variable in this TDZ throws a ReferenceError.

Here's an example with an explicit block that shows the start and end of foo's TDZ:

{
     // Start of foo's TDZ
      let bar = 'bar';
    console.log(bar); // "bar"

    console.log(foo); // ReferenceError because we're in the TDZ

    let foo = 'foo';  // End of foo's TDZ
}

The TDZ is also present in default function parameters, which are evaluated left-to-right. In the following example, bar is in the TDZ until its default value is set:

function foobar(foo = bar, bar = 'bar') {
  console.log(foo);
}
foobar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization

But this code works because we can access foo outside of its TDZ:

function foobar(foo = 'foo', bar = foo) {
  console.log(bar);
}
foobar(); // "foo"

"typeof" in the temporal dead zone

Using a let or const variable as an operand of the typeof operator in the TDZ will throw an error:

console.log(typeof foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization
let foo = 'foo';

This behavior is consistent with the other cases of let and const in the TDZ that we've seen. The reason that we get a ReferenceError here is that foo is declared but not initialized – we should be aware that we're using it before initialization (source: Axel Rauschmayer).

However, this isn't the case when using a var variable before declaration because it is initialized with undefined when it is hoisted:

console.log(typeof foo); // "undefined"
var foo = 'foo';

Furthermore, this is surprising because we can check the type of a variable that doesn't exist without an error. typeof safely returns a string:

console.log(typeof foo); // "undefined"

In fact, the introduction of let and const broke typeof's guarantee of always returning a string value for any operand.

Function hoisting in JavaScript

Function declarations are hoisted, too. Function hoisting allows us to call a function before it is defined. For example, the following code runs successfully and outputs "foo":

foo(); // "foo"

function foo() {
    console.log('foo');
}

Note that only function declarations are hoisted, not function expressions. This should make sense: as we just learned, variable assignments aren't hoisted.

If we try to call the variable that the function expression was assigned to, we will get a TypeError or ReferenceError, depending on the variable's scope:

foo(); // Uncaught TypeError: foo is not a function
var foo = function () { }

bar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization
let bar = function () { }

baz(); // Uncaught ReferenceError: Cannot access 'baz' before initialization
const baz = function () { }

This differs from calling a function that is never declared, which throws a different ReferenceError:

foo(); // Uncaught ReferenceError: baz is not defined

Summary:

Thanks for reading, and I hope this post helped you learn about hoisting in JavaScript. Feel free to reach out to me on LinkedIn if you want to connect or have any questions!

Did you find this article valuable?

Support Saket Kumar Jha by becoming a sponsor. Any amount is appreciated!