Understanding Hoisting in JavaScript

You know JavaScript, but do you really know JavaScript? It's a great language, even though some may argue otherwise. Sure, it's got some bad parts, but it has improved a lot in past years and developers are getting much better at using JavaScript correctly and at following best practices. Strict mode is also getting better at preventing newer developers from making some bad JavaScript mistakes and unfortunately running into unwanted behaviours.

However, not everyone has heard of the term Hoisting or knows what it means. In this article, I'll explain what hoisting is and show different examples so that you can better understand what it's all about.

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(bar); // undefined
var bar = 'bar';
console.log(bar); // 'bar'

At first, you may think that the sample code would throw a ReferenceError on line 3 (console.log(bar);) because bar has not been declared yet. However, with the magic of hoisting, it won't throw a ReferenceError but the value of bar 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 bar;
console.log(bar); // undefined
bar = 'bar';
console.log(bar); // 'bar'

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

Function Declarations

Hoisting also applies to function declarations (not function expressions). Let's analyze the following sample code:

'use strict';

foo();
function foo() {
    console.log(bam); // undefined
    var bam = 'bam';
}

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

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

However, the important thing to note here is that bam 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.

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

'use strict';

function foo() {
    var bam;
    console.log(bam); // undefined
    bam = 'bam';
}

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

Notice how foo() was moved to the top, and bam is declared in foo()? This means that, when you call console.log(bam) on line 10, it will not find the variable bam in the general scope and will throw a ReferenceError.

Function Expressions

Next, 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. Here's some sample code to demonstrate my point:

'use strict';

foo();
var foo = function () {
    console.log(bam); // undefined
    var bam = 'bam';
}

This code throws a TypeError: foo is not a function error since only the variable declaration var foo is hoisted to the top of the file, and the assignment of the function to foo 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 foo;
foo(); // `foo` has not been assigned the function yet
foo = function () {
    console.log(bam);
    var bam = 'bam';
}

What Takes Precedence?

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

'use strict';

console.log(typeof foo); // 'function'

var foo = 'foo';

function foo () {
    var bam = 'bam';
    console.log(bam);
}

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

On the first run, the interpreter will hoist foo() at the top of the current scope, and then will get to the var foo = 'foo' line. At that point, it realizes that foo 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 foo before it gets to the assignment foo = 'foo'.

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

'use strict';

function foo () {
    var bam = 'bam';
    console.log(bam);
}

console.log(typeof foo); // 'function'
foo = 'foo';

ES6

ES6 is the future and is what most developers will be using moving forward, so let's see how hoisting applies for ES6 code.

Hoisting doesn't apply the same way for let and const variables compared to var variables, as we saw above. However, let and const variables are still hoisted, the difference being that they cannot be accessed until the assignment is done at runtime.

From ES6's documentation:

The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated.

At the end of the day, it's a small technicality where the interpreter applies hoisting to these variables on the compile run but they'll throw reference errors when accessed before the assignment happens, essentially preventing us from accessing these variables before their assignment.

Conclusion

I hope this clarifies how hoisting works in JavaScript. It's definitely not as tricky or complicated as it sounds, but it does require us to breakdown the different use cases and trying different scenarios to understand how things work under the hood.

Do not hesitate to leave me comments or questions if you have any - I'd love to hear your feedback.