Code

Once More Unto the Breach: Another Explanation of Hoisting

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:

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:

Here’s the way this code actually runs:

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:

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:

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:

Now that’s interesting. It looks like global variables are hoisted behind the scenes after all! But let’s check one more thing:

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:

Is equivalent to this:

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:

This code is equivalent to:

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:

One might expect this one to return 20, but it does not. The above code is equivalent to this:

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:

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.