JavaScript hoisting enables functions and variables to be used before they are declared. This behaviour has a few nuances that will be explained further in this article, along with examples of how to avoid potential errors that can occur as a result.
When a function or variable is hoisted in JavaScript, it means that it is possible to access it at the top of its containing scope. This can be either the global scope or the local scope. The following two examples demonstrate hoisting at it's most basic:
console.log(foo); // Output: undefinedvar foo = "Hoisted variable";
In the above example, the JavaScript interpreter has split the variable declaration and assignment and, in effect, pulled the declaration to the top of the scope. The interpreter also initializes the variable with the value of undefined
, which is the result that gets output at the point that it is logged.
foobar(); // Output: Hoisted functionfunction foobar() { console.log("Hoisted function");}
In the case of the above function, the javascript interpreter has again pulled the declaration to the top of the scope. As this is a function declaration, not an assignment, the function can be called and provide a result before it's declared. The reason behind this behaviour is because the javascript interpreter interprets code in two stages:
- Creation Stage: This stage creates all the scopes, variables and functions. It also sets the 'this' context.
- Execution Stage: This stage executes the code by sending machine understandable commands. Now, let's go more in-depth and look at variable and function hoisting with some more examples.
Variable hoisting
Let's start by looking at some other types of variables. In addition to var
we also have let
and const
. These don't behave the same way as var
. The following examples show what happens when these types of variables are called before their declaration:
console.log(a); // ReferenceError: a is not definedlet a = "a";
console.log(b); // ReferenceError: b is not definedconst b = "b";
In both cases, calling variables declared with either let
or const
before they are declared will result in a reference error being thrown. This is because, while the interpreter is hoisting these too, it is not initializing them and this is why calling them results in an error.
Another scenario to look at is how variables are hoisted within functions. The following example shows what happens when a var
declared inside a function is called before its declaration:
console.log(bar); // ReferenceError: bar is not definedfunction foo() { console.log(bar); // Output: undefined var bar = "bar";}foo();
The var
is hoisted to the top of the function and initialized with a value of undefined
, but this does not mean its value is accessible outside the function. An error is thrown when trying to access the variable outside the function.
This behaviour is different when using other variable types such as let
and const
.
console.log(bar); // ReferenceError: bar is not definedfunction foo() { console.log(bar); // ReferenceError: bar is not defined let bar = "bar";}foo();
The let
declared inside the function is not accessible outside of the function as a reference error is thrown when calling it, and the same happens when accessing it within the function before it's declared. This behaviour is also consistent when declaring the variable with const
.
Function hoisting
We saw earlier that function declarations are hoisted to the top of their scope. Now let's take a look at function expressions.
expression(); //Output: "TypeError: expression is not a functionvar expression = function () { console.log("Function expression");};
In the above example, calling the function before it has been declared results in a type error being thrown. This is because, while the variable is hoisted to the top of its scope, it's not initialized with a function value and so trying to call it results in an error. This behaviour is consistent when using arrow functions as well:
expression(); //Output: "TypeError: expression is not avar expression = () => { console.log("Function expression");};
Order of precedence
When declaring variables and functions, it's important to consider the order of precedence as this will change the behaviour.
The following example shows what happens when a var
and a function declaration with the same name are declared in the global scope:
var example = "I am a variable";function example() { return "I am a function";}console.log(typeof example); // Output: string
The example
variable remains a string because variable assignment takes precedence over function declaration. In the following example, the variable assignment is removed, but the variable is still declared before the function:
var example;function example() { return "I am a function";}console.log(typeof example); // Output: function
Now, the example
variable is of type function, and this is because function declarations take precedence over variable declarations.
Conclusion
Hopefully, you now have a good understanding of how hoisting works in JavaScript. Keep in mind that hoisting only moves declarations to the top of their scope and does not initialize variables with values. It's important to be aware of this behaviour as it can lead to unexpected results if not considered. When declaring functions and variables, take into account the order of precedence as this will change the outcome.