JavaScript

Understanding Hoisting in Javascript

Updated:

Hoisting in one of the more 'advanced' topics in JavaScript, but every JS developer needs to understand it. It is one of the odd 'quirks' or 'weird parts' of the language. This concept can get quite technical because it has to do with how the JS engine compiles and executes JS code. Before digging into the details, let's look at some simple examples.

function greeting(name) {
  console.log("Hello " + name);
}

greeting("John");

/*
  The result of the code above is: "Hello John"
*/

We would expect that after running this code, we would see the string 'Hello John' printed to the console. What would happen if we switch the function invocation and the function declaration order?

greeting("John");

function greeting(name) {
  console.log("Hello " + name);
}

/*
  The result of the code above is: "Hello John"
*/

Surprisingly we get the same result! How does the greeting function still execute even though it is not declared until after we invoke it? The answer... hoisting!


Aside: Defining terms

Before we get into the technical details of hoisting, I want to make sure I clarify some terminology I used above. A function declaration is when you write and create a function, like so:

// Function declaration
function greeting(name) {
  console.log("Hello " + name);
}

A function invocation is when you 'invoke' or execute the function. Like so:

// Function invocation or 'invoking the function'
greeting("John");

How hoisting works (the technical parts)

Here is a definition from the MDN docs on hoisting.

Conceptually, for example, a strict definition of hoisting suggests that variable and function declarations are physically moved to the top of your code, but this is not in fact what happens. Instead, the variable and function declarations are put into memory during the compile phase, but stay exactly where you typed them in your code.

First, did you notice that in their definition, they mention both variables and function declarations. Later on we will look at how variable hoisting works as well, but for now, let's simply focus on function declarations.

A common misconception about hoisting is that many people think that the JS engine, actually moves up or 'hoists' your function declaration to the top of the page. It is easy to understand why, as that is what it appears to be doing.

Let's look at that example again:

greeting("John");

function greeting(name) {
  console.log("Hello " + name);
}

/*
  The result of the code above is: "Hello John"
*/

It is as if the JS engine, is doing this:

function greeting(name) {
  console.log("Hello " + name);
}

greeting("John");

/*
  The result of the code above is: "Hello John"
*/

However, that is not what is happening. To understand what is going on, we need to understand execution contexts.

Execution Context - Creation Phase (Phase 1)

The JS engine creates something that is called the execution context, and is created in two phases. The first phases is called the creation phase.

During this phase of the execution context, the JS engine setups up space in memory for variables and functions. What this means is that before your code begins to be executed line by line, the JS engine has already created space in memory for your variables and functions. Therefore, since the JS engine has these variables and functions already in memory, your code has access to them! This is how 'hoisting' works under the hood.

This is also why it appears like the JS engine has moved your code to the top. It hasn't moved anything, the engine already knows about your variables and functions, and when your code is executed it finds them already existing in memory.

Execution Context - Execution Phase (Phase 2)

During this phase, the JS engine executes your code from top to bottom, line by line. Let's take a look at a variable example to help solidify the difference between these two phases.

console.log(foo);

var foo = "Hello World!";

/*
  The result of the code above is: undefined
*/

This might not seem all that surprising, but take a look at this example:

console.log(foo);

/*
  The result of the code above is: Uncaught ReferenceError: foo is not defined
*/

So what exactly is going on here? In the first example, when the JS engine is in the creation phase of the execution context it will automatically assign a value of undefined to our variable foo. This is why when we try to console.log(foo) even though foo has not been assigned it prints undefined to the console. You can think of undefined as being a placeholder, until later on when it expects to see foo actually assigned to some value.

In the second example, the reason why we are getting a reference error, is because at first, foo has a value of undefined, however, since foo is never assigned a value later on we get Uncaught ReferenceError: foo is not defined.

So as we can see by these two examples, the JS engine has already created space in memory for our variable. It does the same for our function declarations.

It is always best to not rely upon hoisting and to write your code in such a way as to not utilize it. It will make your code easier to read, reason about, and debug.

Preventing function hoisting

There are a couple of ways to prevent hoisting of our functions. The first, the most obvious, is to declare our functions before executing them.

Like so:

function greeting(name) {
  console.log("Hello " + name);
}

greeting("John");

/*
  The result of the code above is: "Hello John"
*/

Function Expression

The other method is to use what is known as a function expression. A function expression is when you both declare & assign a function at the same time. Like so:

var greeting = function (name) {
  console.log("Hello " + name);
};

An easy way to remember the difference between a function declaration & a function expression is that function expressions have an = to the left of them. Function declarations do not have an = sign, only function expressions do.

Let's see if this will prevent hoisting:

greeting("John");

var greeting = function (name) {
  console.log("Hello " + name);
};

/*
  The result of the code above is: Uncaught TypeError: greeting is not a function
*/

It does! Since we are both declaring & assigning this function at the same time, the JS engine does not 'hoist' it.

Variable Hoisting

Let's now look at another example using both a function and variables.

function hoisting() {
  a = "foo";
  var b = "bar";
}

hoisting();
console.log(a);
console.log(b);

/*
  The result of the code above is:
  foo
  Uncaught ReferenceError: b is not defined
*/

This is because the variable a is undeclared, and therefor the JS engine creates it as a global variable. All undeclared variables are global variables.

Variable b however is not defined because b is declared, and also, it's scope is limited to within the body of the hoisting() function.

b is therefore not accessible outside of the function.

The Let Keyword

In an earlier example, we saw the following:

console.log(foo);

var foo = "Hello World!";

/*
  The result of the code above is: undefined
*/

If we instead use the let keyword, look at what happens:

console.log(foo);

let foo = "Hello World!";

/*
  The result of the code above is: 
  Uncaught ReferenceError: Cannot access 'foo' before initialization
*/

This newer keyword in JS will prevent us from getting back undefined and will throw a reference error. This is why you should probably be using the let keyword or the const keyword instead of var going forward.

Wrap Up

I hope this helps clarify the issues & confusion around hoisting. Knowing this will help you to write better code that is easier to read, reason about & debug.

Previous
Looping through objects