In JavaScript, hoisting allows you to use functions and variables before they're declared. In this post, we'll learn what hoisting is and how it works.
What is hoisting?
Take a look at the code below and guess what happens when it runs:
console.log(foo);
var foo = 'foo';
It might surprise you that this code outputs undefined
and doesn't fail or throw an error – even though foo
gets assigned after we console.log
it!
This is because the JavaScript interpreter splits the declaration and assignment of functions and variables: it "hoists" your declarations to the top of their containing scope before execution.
This process is called hoisting, and it allows us to use foo
before its declaration in our example above.
Let's take a deeper look at functions and variable hoisting to understand what this means and how it works.
Variable hoisting in JavaScript
As a reminder, we declare a variable with the var
, let
, and const
statements. For example:
var foo;
let bar;
We assign a variable a value using the assignment operator:
// Declaration
var foo;
let bar;
// Assignment
foo = 'foo';
bar = 'bar';
In many cases, we can combine declaration and assignment into one step:
var foo = 'foo';
let bar = 'bar';
const baz = 'baz';
Variable hoisting acts differently depending on how the variable is declared. Let's begin by understanding the behavior for var
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
How to use hoisting in JavaScript
Variable hoisting
Because of the confusion that var
hoisting can create, it's best to avoid using variables before they're declared. If you're writing code in a greenfield project, you should use let
and const
to enforce this.
If you are working in an older codebase or have to use var
for another reason, MDN recommends that you write var
declarations as close to the top of their scope as possible. This will make the scope of your variables more clear.
You can also consider using the no-use-before-define
ESLint rule which will ensure you don't use a variable before its declaration.
Function hoisting
Function hoisting is useful because we can hide function implementation farther down in the file and let the reader focus on what the code is doing. In other words, we can open up a file and see what the code does without first understanding how it's implemented.
Take the following contrived example:
resetScore();
drawGameBoard();
populateGameBoard();
startGame();
function resetScore() {
console.log("Resetting score");
}
function drawGameBoard() {
console.log("Drawing board");
}
function populateGameBoard() {
console.log("Populating board");
}
function startGame() {
console.log("Starting game");
}
We immediately have an idea of what this code does without having to read all the function declarations.
However, using functions before their declaration is a matter of personal preference. Some developers, such as Wes Bos, prefer to avoid this and put functions into modules that can be imported as needed (source: Wes Bos).
Airbnb's style guide takes this further and encourages named function expressions over declarations to prevent reference before declaration:
Function declarations are hoisted, which means that it’s easy - too easy - to reference the function before it is defined in the file. This harms readability and maintainability.
If you find that a function’s definition is large or complex enough that it is interfering with understanding the rest of the file, then perhaps it’s time to extract it to its own module! (Source: Airbnb JavaScript Style Guide)
Conclusion
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!