Hoisting in JavaScript is often misunderstood to mean that certain bits of code get moved to the top of the page before they are executed. This is not so; what actually happens is that code does not always run in the order it appears on the page. Hoisted code runs before non-hoisted code, no matter what the order is on the page (the lexical order).
Creation phase vs. execution phase
The reason that this can happen is that JavaScript runs in two phases: the creation phase and the execution phase. The creation phase runs first in its entirety before the execution phase runs.
Declaration of variables, constants and functions happens in the creation phase. The execution phase assigns values to variables and constants and invokes functions.
Since the creation phase precedes the execution phase, statements that the creation phase handles run before statements that the execution phase handles, no matter where they occur from a lexical standpoint.
This gives the perception that creation-phase code “moves up to the top” of the code, above execution-phase code. While this does not actually happen in any sort of lexical sense, it does happen in a temporal sense: “hoisted” code is not “rewritten” in any sort of new order, but it does run earlier than the order in which it’s written.
So, perhaps “processed before” is a more accurate expression than “hoisted above.”
Effect of Hoisting on Variables and Constants
Block-Level Variables and Constants
Variables declared with let
and constants (declared with const
) have block-level scope. (For more on block scoping, see the MDN doc on blocks.)
Variables declared with let
and constants are not hoisted. They are declared in the creation phase, but they are not initialized until they have values assigned to them in the execution phase.
The area between their declaration and their initialization is called the temporal dead zone. Attempts to access variables from the temporal dead zone will result in errors.
Here is an example:
1 2 3 4 |
console.log(x); // ReferenceError: Cannot access 'x' before initialization let x = 10; |
What happens here is that x
is declared in the creation phase, but (unlike var
variables) it is not initialized to undefined
. It is in a temporal dead zone until it is initialized on line 2, so attempts to access it on line 1 result in this error.
Variables declared with var
Some code will show how hoisting affects var
variables:
1 2 3 4 5 |
var x = 10; console.log(x, y); // 10 undefined var y = 20; |
Here’s the way this code actually runs:
1 2 3 4 5 6 7 |
var x; var y; x = 10; console.log(x, y); y = 20; |
The declarations of x
and y
are hoisted: in the creation phase, they are declared and initialized to undefined
. The assignment of 10
to x
, the invocation of the log
method of the console
object, and the assignment of 20
to y
are processed in that order in the execution phase.
Thus, while the log
invocation does not precede the declaration of y
, since declarations happen in the creation phase, it does precede the assignment of 20
to y
, since assignments happen in the execution phase. Therefore, y
‘s value is still undefined
when console.log
is invoked.
Global Variables
Global variables are variables with no explicit declaration, for example x = 10;
.
How JavaScript Implements Global Variables
All JavaScript implementations have a global object. Global variables are actually attributes of this global object. So, for example the statement x = 10;
adds an attribute x
to the globlal object and assigns the value 10
to it.
The name of the global object varies from implementation to implementation. For example, in node.js, the global object is called global
. In Chrome and most other browsers, the global object is called window
.
To allow for greater standardization, the ES11 spec (June 2020) defined a globalThis
property. This property is a platform-independent reference to the global object, and is itself a property of the global object. For example, in node.js, global.globalThis === global
evaluates to true
, while in Chrome, window.globalThis === window
evaluates to true
. See the MDN doc for more information.
Global Variables and Hoisting
Global variables are hoisted. But on the face of it, it doesn’t look that way. Let’s start with this example:
1 2 3 4 5 |
x = 10; console.log(x); // 10 |
This is typical, and hoisting doesn’t apply here. On encountering x = 10
, x
is implicitly declared as a property of the global object, and the value 10
is assigned to it. Logging x
on the next line duly logs a 10
.
The question is whether globalThis.x
gets initialized in the creation phase or in the execution phase. To figure that out, we can start here:
1 2 3 4 5 |
console.log(y); y = 20; // ReferenceError: y is not defined |
This suggests that y
is not hoisted, because the first line is trying to log the value of y
and y
hasn’t been declared yet. If y
were hoisted, the first line would log undefined
, because y
would be created and initialized but not assigned — again, because creation and initialization happen in the creation phase and assignment happens in the execution phase.
But let’s look a little further:
1 2 3 4 5 |
console.log(globalThis.y); y = 20; // undefined |
Now that’s interesting. It looks like global variables are hoisted behind the scenes after all! But let’s check one more thing:
1 2 3 4 5 6 7 |
console.log(globalThis.hasOwnProperty('y')); y = 20; console.log(globalThis.hasOwnProperty('y')); // false // true |
This shows that there is no global property y
when the first log
method is invoked, and there is one after initialization of the y
variable. It’s misleading because if you try to reference an object property that hasn’t been defined, you will get undefined
, not a reference error, so it looks like initialization has taken place.
From this we can conclude that global variables aren’t hoisted.
Hoisting and Functions
Function declarations
Function declarations are hoisted in their entirety; both the function declaration and the body of code in the function block are placed into memory in the creation phase. So this:
1 2 3 4 5 6 7 |
function test() { console.log('test'); } test() // test |
Is equivalent to this:
1 2 3 4 5 6 7 |
test(); // test function test() { console.log('test'); } |
Function declarations are processed before function invocations, no matter the order in which they are written.
Since the entire test
function is created in the creation phase, it is available when test
is invoked in the execution phase, regardless of the order in which the declaration and invocation appear in the code.
Function expressions
Function expressions are variables or constants with functions assigned to them. They follow the hoisting rules of the variable or constant to which the function is assigned. However, since assignment happens in the execution phase, the functions themselves are never hoisted:
1 2 3 4 5 6 7 8 |
console.log(test); // undefined test(); // TypeError: test is not a function var test = function() { console.log('test'); } |
This code is equivalent to:
1 2 3 4 5 6 7 8 |
console.log(test); var test; test(); test = function() { console.log('test'); } |
In this case, test
is not yet a function when the attempt is made to invoke it. It is a variable that has been initialized with a value of undefined
.
Scoping concerns
Functions have their own execution context. This means that when a function is invoked, it has its own creation and execution phases. Combining this with scoping details can create some counterintuitive scenarios.
For example, what this code does may not be immediately obvious:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var x = 10; function test() { if (x === 10) { var x = 20; } return x; } console.log(test()); // undefined |
One might expect this one to return 20
, but it does not. The above code is equivalent to this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var x; function test() { var x; if (x === 10) { x = 20; } return x; } x = 10; console.log(test()); // undefined |
The part that’s easy to miss here is that when test
is invoked, the x
inside the if
block is hoisted, so it’s declared and initialized to undefined
in test
‘s creation phase. So, in the execution phase, x === 10
evaluates to false
, since its value is undefined
. So x = 20
is not executed, and when the function returns x
it returns undefined
.
Summary
Hoisting is best understood as happening in a temporal sense, rather than in a lexical sense. None of the code is actually “hoisted” up to the top of the page, but code is not necessarily processed in the order in which it is written (in lexical order). This is because the creation phase happens before the execution phase.
Understanding what happens in each of the two phases is the key to understanding hoisting. Since the creation phase runs first, things that happen in the creation phase that are written later in the code than things that happen in the execution phase are executed first and are considered to be hoisted.
Hoisting is usually unintentional, and it is good practice to find ways to prevent it. Using block scoping over var
declarations reduces the circumstances under which hoisting occurs.
We can demonstrate this in very simple terms:
1 2 3 4 5 6 7 8 9 |
console.log(x); var x; // undefined console.log(y); let y; // ReferenceError: Cannot access 'y' before initialization |
The temporal free zone makes it easier to find errors, because an unintentional attempt to access a variable before assigning a value to it results in an error rather than an undefined
value, which one could plug into something by mistake and create a hard-to-find logical error.
These are some of the reasons that it is increasingly considered best practice to prefer let
and const
over var
. Using block scoping for variables removes potential hoisting issues from variable declarations. In other words, using block scoping confines hoisting issues to function declarations.