By Dillion Megida
Hoisting is a concept or behavior in JavaScript where the declaration of a function, variable, or class goes to the top of the scope they were defined in. What does this mean?
Hoisting is a concept you may find in some programming languages (such as JavaScript) and not in others. It's a special behavior of the JavaScript interpreter. We'll learn about how it works in this article.
Let's start with functions.
I have a video version of this topic which you can check out.
Function Hoisting
Take a look at this code example:
function printHello() {
console.log("hello")
}
printHello()
// hello
Here, we declare printHello
, and we execute the function just after the line it was declared. No errors; everything works!
Now look at this example:
printHello()
// hello
function printHello() {
console.log("hello")
}
Here, we execute printHello
before the line the function was declared. And everything still works without errors. What happened here? Hoisting.
Before the interpreter executes the whole code, it first hoists (raises, or lifts) the declared function to the top of the scope it is defined in. In this case, printHello
is defined in the global scope, so the function is hoisted to the top of the global scope. Through hoisting, the function (including the logic) becomes accessible even before the line it was declared in the code.
Let's see another example:
printHello()
// hello
printDillion()
// ReferenceError: printDillion is not defined
function printHello() {
console.log('hello')
function printDillion() {
console.log("dillion")
}
}
As you see here, we declare printHello
. In this function, we first execute console.log('hello')
then we declare another function called printDillion
which executes console.log('dillion')
when called.
Before printHello
is declared in the code, we try to access it by executing printHello()
. It's accessible (since it is hoisted to the top of the global scope), so we have "hello" printed on the console.
But then we try to access printDillion
, and we get a reference error: printDillion is not defined. Does hoisting not occur on printDillion
?
printDillion
is hoisted, but it is only lifted to the top of the scope it was declared in. In this case, it is declared in a local scope--in printHello
. Therefore, it would only be accessible in the function. Let's update our code:
printHello()
// hello
function printHello() {
printDillion()
// dillion
console.log('hello')
function printDillion() {
console.log("dillion")
}
}
Now, we execute printDillion
in printHello
before the line where printDillion
was actually declared. Since the function is hoisted to the top of the local scope, we're able to access it before the line where it was actually declared.
Hoisting makes all these possible for function declarations. But, it's also worth noting that hoisting does not occur on function expressions. I explained the reason for this here: Function Declarations vs Function Expressions
Now let's look at hoisting for variables.
Variable Hoisting
You can declare variables in JavaScript with the var
, let
, and const
variables. And these variable declarations would be hoisted, but behave differently. Let's start with var
.
Hoisting var
variables
Take a look at this example:
console.log(name)
// undefined
var name = "Dillion"
Here, we declare a variable called name
with a string value of "Dillion". But, we try to access the variable before the line it was declared. But we don't get any errors. Hoisting happened. The name
declaration is hoisted to the top, so the interpreter "knows" that there is a variable called name
. If the interpreter did not know, you would get name is not defined. Let's try it out:
console.log(name)
// ReferenceError: name is not defined
var myName = "Dillion"
We have a variable called myName
but no name
. We get the name is not defined error when we try to access name
. The interpreter "does not know" about this variable.
Coming back to our example above:
console.log(name)
// undefined
var name = "Dillion"
Although hoisting happened here, the value of name
is undefined when we access it before the line of declaration. With variables declared var
, the variable declaration is hoisted but with a default value of undefined
. The actual value is initialized when the declaration line is executed.
By accessing the variable after that line, we get the actual value:
console.log(name)
// undefined
var name = "Dillion"
console.log(name)
// Dillion
Let's say we declared name
in a function:
print()
console.log(name)
// ReferenceError: name is not defined
function print() {
var name = "Dillion"
}
Here, we get a reference error: name is not defined. Remember, variables are hoisted but only to the top of the scope they were declared in. In this case, name
is declared in print
, so it will be hoisted to the top of that local scope. Let's try to access it in the function:
print()
function print() {
console.log(name)
// undefined
var name = "Dillion"
}
By trying to access name
in the function, even though it's above the line of declaration, we do not get an error. That's because name
is hoisted, but don't forget, with a default value of undefined
.
Hoisting let
variables
Although variables declared with let
are also hoisted, they have a different behavior. Let's see an example:
console.log(name)
// ReferenceError: Cannot access 'name' before initialization
let name = "Dillion"
Here, we get a reference error: Cannot access 'name' before initialization. Do you notice that the error does not say name is not defined? That's because the interpreter is "aware" of a name
variable because the variable is hoisted.
"Cannot access 'name' before initialization" occurs because variables declared with let
do not have a default value when hoisted. As we saw in var
, the variables have a default value of undefined
until the line where the declaration/initialization is executed. But with let
, the variables are uninitialized.
The variables are hoisted to the top of the scope they are declared in (local, global, or block), but are not accessible because they have not been initialized. This concept is also referred to as the Temporal Dead Zone.
They can only be accessible after the initialization line has been executed:
let name = "Dillion"
console.log(name)
// Dillion
By accessing name
after the line it was declared and initialized, we get no errors.
Hoisting const
variables
Just like let
, variables declared with const
are hoisted, but not accessible. For example:
console.log(name)
// ReferenceError: Cannot access 'name' before initialization
const name = "Dillion"
The same concept of a temporal dead zone applies to const
variables. Such variables are hoisted to the top of the scope they are defined in (local, global, or block), but they remain inaccessible until the variables are initialized with a value.
const name = "Dillion"
console.log(name)
// Dillion
By accessing the variable after it has been initialized with a value (as you see above), everything works fine.
Moving onto hoisting for classes.
Class Hoisting
Classes in JavaScript are also hoisted. Let's see an example:
const Dog = new Animal("Bingo")
// ReferenceError: Cannot access 'Animal' before initialization
class Animal {
constructor(name) {
this.name = name
}
}
Here, we declare a class called Animal
. We try to access this class (instantiate a Dog
object) before it was declared. We get a reference error: Cannot access 'Animal' before initialization. What does this error remind you of?
Just like let
and const
variables, classes are hoisted to the top of the scope they are defined in, but inaccessible until they are initialized. We do not get "Animal is not defined", which shows that the interpreter "knows" that there is an Animal
class (due to hoisting). But we cannot access that class until the line of initialization is executed.
Let's update the code:
class Animal {
constructor(name) {
this.name = name
}
}
const Dog = new Animal("Bingo")
console.log(Dog)
// { name: 'Bingo' }
After Animal
has been initialized, it becomes accessible, so we can instantiate the Dog
object from the class without errors.
Wrap Up
In some codebases, you may find a code similar to this:
function1()
function2()
function3()
// lines of code
// lines of code
function function1() {...}
function function2() {...}
function function3() {...}
The three functions here are called at the top but actually declared in the code at the bottom. This is possible, due to hoisting. The functions are hoisted to the top of the global scope (which is where they were defined) together with their logic, so they become accessible/executable even before the line they were defined.
It's different for the others. var
variables are hoisted but with a default value of undefined
. let
and const
variables, and classes are hoisted but inaccessible as they do not have a default initialization.
If you enjoyed this piece, please share it with others 🙏🏾