Hoisting in JavaScript

Published on 7th May, 2020 5 minutes read javascript

Topic of discussion today is "hoisting", JavaScript's default behavior of moving variable and function declarations to the top during compilation. Hoisting coupled with other concepts like scope can sometimes behave in very weird ways and can result in bugs, if not taken seriously. In this article, I'll not only show you scenarios where this can be really confusing but also will give suggestions based on my experiences.

Introduction

Code can express more than words, so let me start by showing you the following code snippet:

console.log(name);

// ReferenceError: name is not defined

Fair enough, that variable name has never been declared so a ReferenceError exception is completely expected. Now look at the following code snippet and guess the output:

console.log(name);

var name;

// undefined

Given you are trying to access the variable name before even declaring it, you should get another ReferenceError thrown at you right away. But what you get instead is a mere undefined in the console.

This kind of anomalies can happen with functions as well. Look at the following code:

sayName();

function sayName() {
  console.log('I am Watson');
}

// I am Watson

The output is "I am Watson" whereas a ReferenceError is what should have been thrown at you right away

As can be seen in all the above code snippets, JavaScript makes variables and functions available before their declaration.

How does it happen then?

Hoisting and JavaScript Engine

According to W3Schools, Hoisting is JavaScript's default behavior of moving declarations to the top. Reading this definition one may think that JavaScript physically moves variable and function declarations to the top of the source code during compilation but that's not true. In order to understand what actually happens you need to know a little bit about the JavaScript engine.

JavaScript engine is the program that reads and runs code written by a programmer. It does it's job in two phases:

  • Memory Creation Phase.
  • Execution Phase.

During the Memory Creation Phase, compiler goes through the entire code twice:

  • In the first run, it picks all function declarations and stores them in memory with their reference.
  • In the second run, it picks all variables and assigns undefined to them.

This is the main reason behind variables as well as functions being accessible even before they've been declared. Now that you know, what happens inside the JavaScript engine, you understand that declarations are not physically moved, rather they get initialized and put into memory.

Variable Hoisting

Guess the output for following code snippet:

console.log(name);

var name = 'Watson';

console.log(name);

// undefined
// Watson

Although the variable has been initialized to Watson, it prints out undefined because by the time execution reaches the first console.log() statement, the engine has initialized that with undefined. But by the time execution reaches that second console.log() statement, your initialization has already taken place and Watson gets printed out on the console.

console.log(name);

if (true) {
  var name = 'Watson';
}

console.log(name);

// undefined
// Watson

The only way to create a block level scope in JavaScript (at least pre ES6) is by declaring a function so, variables inside if blocks, get treated as global variables and also get hoisted.

But what happens inside a function scope? Does hoisting take place there? Look at the following code:

console.log(name);

(function() {
  var name = 'Watson';
})()

// ReferenceError: name is not defined

As I've already stated, function creates a new block level scope and thus name variable didn't get hoisted.

But if we go inside the function scope however things get a bit complicated:

(function() {
  console.log(name);

  var name = 'Watson';

  console.log(name);
})()

// undefined
// Watson

That means variables get hoisted within the scope they are in. Scope in itself is a pretty easy thing to understand. But scope mixed up with hoisting can create some confusions.

Hoisting within Scope

Let me show you an example:

var name = 'Watson';

function sayName() {
  console.log(`Inner Scope: ${name}`);

  var name = 'Sherlock';
  console.log(`Inner Scope: ${name}`);
}

console.log(`Outer Scope: ${name}`);
sayName();
console.log(`Outer Scope: ${name}`);

// Outer Scope: Watson
// Inner Scope: undefined
// Inner Scope: Sherlock
// Outer Scope: Watson

Out of the four console.log() statements, the second one is a bit weird. Because theoretically it should have outputted Inner Scope: Watson, taking the value from it's outer scope. But what you've got is undefined.

The reason behind this is pretty simple though. By the time execution has reached the first console.log() statement inside the function, the engine has hoisted the inner name variable, initializing that with a value of undefined.

But by the time execution has reached the second console.log() statement inside the function, your initialization has taken place and you got Inner Scope: Sherlock as output.

So the next time you work with variables across multiple scopes, be careful about hoisting.

Function Hoisting

Just like variable declarations, function declarations get hoisted as well.

sayName();

function sayName() {
  console.log('I am Watson');
}

// I am Watson

Now that you know about hoisting and also a little bit about the JavaScript engine, you may think the output is fair enough, the function declaration is getting hoisted making it accessible. But what would you say about the following code snippet then?

sayName();

var sayName = function() {
  console.log('I am Watson');
}

// TypeError: sayName is not a function

This is a slightly modified version of the previous code snippet. In this, I'm using function expression instead of function declaration and that's making all the difference.

Function Declaration vs Function Expression

As you may already know that there are two ways of defining functions in JavaScript:

  • Function Declaration.
  • Function Expression.

A function declaration starts with the function keyword, followed by the function name, parameters inside a set of parentheses and then the block of code to be executed inside a set of curly braces.

On the other hand, a function expression allows you to create a function without name. Starting with the function keyword, followed by parameters inside a set of parentheses and the block of code to be executed inside a set of curly braces. A function expression can be assigned to a variable and can be executed by calling that variable name.

Although both syntax result in functions that more or less work the same, JavaScript engine treats them very differently. Let's go back to the function expression snippet again:

sayName();

var sayName = function() {
  console.log('I am Watson');
}

// TypeError: sayName is not a function

When JavaScript engine encounters the var keyword it expects a variable declaration ahead and assigns undefined to the variable. When the execution reaches sayName() statement, it contains undefined hence the TypeError: sayName is not a function exception.

ES6 let and const

First introduced in ES6, let and const keywords solve all these confusions regarding hoisting. Variables declared using let and const do not get hoisted and also take block level scopes in account.

console.log(name);

let name;

// ReferenceError: Cannot access 'name' before initialization

This is the second code example that you saw earlier, using let instead of var. Running this code snippet you'll get a ReferenceError just as one may expect.

if (true) {
  let name = 'Watson';
}

console.log(name);

// ReferenceError: name is not defined

With let and const, those if statements are now considered a block level scope thus variables declared inside them stays in the local scope.

Both let and const works the same way except variables declared with const can not be reassigned.

Conclusion

Hoisting was and still is one of the most confusing aspects of JavaScript but is not very hard to wrap your head around. As ES6 is now supported in all of the modern browsers, using let and const can minimize your possibility of mistakes.

I mostly use const in my code. In situations where variables need to be reassigned, I opt for let instead. Usage of var in my code is almost nonexistent at this point but you may use what you prefer as long as you understand what you're doing.