DEV Community

Cover image for Hoisting

Hoisting

One of the trickier aspects of JavaScript for new JavaScript developers is the fact that variables and functions are "hoisted." Rather than being available after their declaration, they might actually be available beforehand. How does that work? Let's take a look at variable hoisting first.

// ReferenceError: noSuchVariable is not defined
console.log(noSuchVariable);
Enter fullscreen mode Exit fullscreen mode

❗The JavaScript Interpreter

When you execute your JavaScript code, the interpreter goes through the code twice.

  • The first run through the code is where it does a safety check and small optimizations of your code. Safety checks such as making sure that the syntax is right, if there are any calls to eval or with, etc. Then, it optimizes the code as best as it can to ensure better performance when it is executed. This is also where hoisting occurs (more on this soon). This is also referred to as the compile run.

  • The second run is where it actually executes your code by going through it line by line, doing the assignments, calling the functions, and so on.

❗ What is Hoisting?

πŸ›‘ Hoisting is when the JavaScript interpreter moves all variable and function declarations to the top of the current scope. It's important to keep in mind that only the actual declarations are hoisted, and that assignments are left where they are.

πŸ‘‰ Hoisting is done during the interpreter's first run through the code.

Variable Declarations

Let's start with a basic example and look at the following code:

'use strict';

console.log(book);
var bar = 'book';
console.log(book);

// output: undefined , book
Enter fullscreen mode Exit fullscreen mode

At a first glance, you may think that the code would throw a ReferenceError on (console.log(book);) because book has not been declared yet.
πŸ‘‰ However, with the magic of hoisting, it won't throw a ReferenceError but the value of book will be undefined at that point. This is because the JavaScript interpreter does a first run through the whole code and declares all variables and functions at the top of the current scope, and then, on the second run, will execute the code.

  • Here's what the same code would look like after the interpreter's first run:
'use strict';
var book;
console.log(book); // undefined
bar = 'book';
console.log(book); // 'book'
Enter fullscreen mode Exit fullscreen mode

Notice how book is now declared at the top (var book) but is not yet assigned at that point? It's a subtle but important difference, and this is why book is logged as undefined instead of throwing a ReferenceError.

❗ Function Declarations

πŸ‘‰ Hoisting also applies to function declarations (not function expressions). Let's analyse the following sample code:

'use strict';

read();
function read() {
    console.log(book);
    var book = 'book';
}

console.log(book);
Enter fullscreen mode Exit fullscreen mode

In this sample code, we are able to successfully call the function read since it's a function declaration and therefore it ishoisted as-is to the top of the current scope.
πŸ‘‰ Then, read will output undefined when calling it since, as in the previous example, book is hoisted to the top of its current scope, which is function read(). This means that book was declared before calling console.log(book) but it has not yet been assigned a value (book = 'book').

πŸ‘‰ πŸ›‘ The important thing to note here is that book was hoisted at the top of its current scope.

  • This means that it was not declared in the global scope but in the function's scope instead.
'use strict';

function read() {
    var book;
    console.log(book); // undefined
    book = 'book';
}

read();
console.log(book); // ReferenceError: book is not defined

Enter fullscreen mode Exit fullscreen mode

πŸ›‘ ❗ Pay attention ❗❗

  • How read() was moved to the top, and book is declared in read()? This means that, when you call console.log(book), it will not find the variable book in the general scope and will throw a ReferenceError.

❗ Function Expressions

the third use case I'd like to cover is how function expressions are not hoisted as opposed to function declarations. Instead, it's their variable declarations that are hoisted.

'use strict';

book();
var book = function () {
    console.log(read);
    var read = 'read';
}

// output: Uncaught TypeError: book is not a function
Enter fullscreen mode Exit fullscreen mode

This code throws a TypeError: book is not a function error since only the variable declaration var book is hoisted to the top of the file, and the assignment of the function to book is done on the interpreter's second run only.

  • Here's what the same code would look like after the interpreter's first run:
'use strict';

var book;
book(); // `book` has not been assigned the function yet
book = function () {
    console.log(read);
    var read = 'read';
}
Enter fullscreen mode Exit fullscreen mode

What Takes Precedence?

the last use case I'd like to cover is that function declarations are hoisted before variables. Let's have a look at the following code:


'use strict';

console.log(typeof book);

var book = 'book';

function book () {
    var read = 'read';
    console.log(read);
}
//output: function
Enter fullscreen mode Exit fullscreen mode

In this example, typeof book returns function instead of string, even though the function book() is declared after the variable. This is because function declarations are hoisted before variable declarations, so book = 'book' is executed on the second run, after calling typeof book.

On the first run, the interpreter will hoist book() at the top of the current scope, and then will get to the var book = 'book' line. At that point, it realises that book was already declared so it doesn't need to do anything and will continue its first run through the code.

Then, on the second run (which basically executes the code), it'll call typeof book before it gets to the assignment
book = 'book'.

Here's what the same code would look like after the interpreter's first run:

'use strict';

function book () {
    var read = 'read';
    console.log(read);
}

console.log(typeof book); // 'function'
book = 'book';
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ We use var to declare a variable, and we have initialized it with a value. After declaring and initializing, we can access or reassign the variable.

πŸ‘‰ If we attempt to use a variable before it has been declared and initialized, it will return undefined.

// Attempt to use a variable before declaring it
console.log(x);
Enter fullscreen mode Exit fullscreen mode
// Variable assignment
var x = 100;
//Output: undefined
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ However, if we omit the var keyword, we are no longer declaring the variable, only initializing it. It will return a ReferenceError and halt the execution of the script.

// Attempt to use a variable before declaring it
console.log(x);

// Variable assignment without var
x = 100;
//Output: ReferenceError: x is not defined
Enter fullscreen mode Exit fullscreen mode

πŸ›‘ ❗ The reason for this is due to hoisting, a behavior of JavaScript in which variable and function declarations are moved to the top of their scope. Since only the actual declaration is hoisted, not the initialization, the value in the first example returns undefined.

To demonstrate this concept more clearly, below is the code we wrote and how JavaScript actually interpreted it.

// The code we wrote
console.log(x);
var x = 100;

// How JavaScript interpreted it
var x;
console.log(x);
x = 100;
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ JavaScript saved x to memory as a variable before the execution of the script. Since it was still called before it was defined, the result is undefined and not 100. However, it does not cause a ReferenceError and halt the script. Although the var keyword did not actually change location of the var, this is a helpful representation of how hoisting works. This behavior can cause issues, though, because the programmer who wrote this code likely expects the output of xto be true, when it is instead undefined.

We can also see how hoisting can lead to unpredictable results in the next example:

// Initialize x in the global scope
var x = 100;

function hoist() {
  // A condition that should not affect the outcome of the code
  if (false) {
    var x = 200;
  }
  console.log(x);
}

hoist();
// Output: undefined
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ In this example, we declared x to be 100 globally. Depending on an if statement, x could change to 200, but since the condition was false it should not have affected the value of x. Instead, x was hoisted to the top of the hoist() function, and the value became undefined.

πŸ‘‰ This type of unpredictable behavior can potentially cause bugs in a program. Since let and const are block-scoped, they will not hoist in this manner, as seen below.

// Initialize x in the global scope
let x = true;

function hoist() {
  // Initialize x in the function scope
  if (3 === 4) {
    let x = false;
  }
  console.log(x);
}

hoist();
// Output:  true
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Duplicate declaration of variables, which is possible with var, will throw an error with let and const.

// Attempt to overwrite a variable declared with var
var x = 1;
var x = 2;

console.log(x);
//Output: 2
Enter fullscreen mode Exit fullscreen mode
// Attempt to overwrite a variable declared with let
let y = 1;
let y = 2;

console.log(y);
// Output: Uncaught SyntaxError: Identifier 'y' has already been declared

Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ To summarise, variables introduced with var have the potential of being affected by hoisting, a mechanism in JavaScript in which variable declarations are saved to memory. This may result in undefined variables in one’s code.
πŸ‘‰ ❗ The introduction of let and const resolves this issue by throwing an error when attempting to use a variable before declaring it or attempting to declare a variable more than once.

Top comments (0)