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.