What is hoisting and how it works in JavaScript

What is hoisting and how it works in JavaScript

September 25, 2020(updated September 26, 2020)โ†’6 min read

The famed and often confusing term, certainly for any JavaScript developer, and it leaves many scratching their heads; what exactly is hoisting?

Hoisting is a term that describes a mechanism in JavaScript to provide early-access to declarations.

Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution.

Ever wondered why in JavaScript you can interact with your variables and functions before theyโ€™ve been declared? Then read on!

tl;dr

Conceptually speaking, hoisting is the movement of declaractions - variables and functions - to the top of their scope, while assignments are left in place. What really happens, is that during compile time, the declarations are put into memory first, but physically remain in place in your code.

The benefit of doing this is that you get access to functions and variables before theyโ€™re declared in code. This only applies to declarations, not to expressions, and not to initializing an undeclared variable.

foo("bar");
function foo(bar) {
  // do something
}

Itโ€™s important to remember that only declarations are hoisted into memory during compile time, not assignments.

Only declarations are hoisted

JavaScript only hoists the declarations of functions and variables into memory during compile time, not the assignments. This means that if a variable is declared and initialized after using it, the value will be undefined.

console.log(foo) // prints 'undefined' as only the _declaration_ was hoisted
var foo; // declaration
foo = "bar"; // initialization

The following is an example of initialization returning a ReferenceError. In this case, a variable is only initialized, not declared; in order to declare something to be hoisted in JavaScript, it must either be explicitly declared as a var or function, as implicit declaractions (initialization only) wont be hoisted.

console.log(foo); // prints 'ReferenceError: foo is not defined'
foo = "bar"; // only initialization, no declaration, value did not follow var

Itโ€™s important to remember that only the declaration is hoisted, even if a value is assigned.

console.log(foo); // prints 'undefined'
var foo = "bar";

Hoisting variables

JavaScript allows us to declare and initialize our variables simultaneously. However, the engine separates the declaration and initialization of variables, and executes it as separate steps, thus allowing the engine to hoist declarations above initializations.

All function and variable declarations are hoisted to the top of their scope, while variable declarations are processed ahead of function declarations; which is why you can call functions with yet-to-be-declared variables, and get an undefined error.

However, there is a caveat. When initializing a variable, that hasnโ€™t yet been declared yet, the variable is hoisted to the global scope when it is executed, rather than hoisted to its scope, like the function itโ€™s being initialized in. This only happens on execution, not at compile time.

function doSomething() {
  doing = "something";
}

console.log(doing); // ReferenceError: doing is not defined
doSomething();
console.log(doing); // something

This is distinctly different to scoped variable declarations, which only exist within their scope.

function doSomething() {
  var doing = "something";
}

console.log(doing); // ReferenceError: doing is not defined
doSomething();
console.log(doing); // ReferenceError: doing is not defined

The take-away is, that declared variables are hoisted to the top of their scope at compiled time, while undeclared variables are hoised to the global scope during execution.

Declarations using let and const are not hoisted to global space

let and const were introduced in ES6 for scope-based operations, but unlike var, do not get hoisted to global space during compile time. Variables declared with let are block scoped and not function scoped. This is significant, because unlike var, thereโ€™s no risk of variable leakage outside of the scope of execution.

The downside is that const and let do not get hoisted, in local or global scope. Read more about var, const and let.

console.log(foo); // prints 'ReferenceError: foo is not defined'
const foo = "bar";

console.log(bar); // prints 'ReferenceError: bar is not defined'
let bar = "bar";

Strict mode prevents sloppy hoisting

Introduced as a utility in ES5, strict-mode is a way to opt in to a restricted variant of JavaScript, implicitly opting-out of sloppy mode. It introduces different semantics, such as eliminating some silent errors, improves some optimizations and prohibits some syntax, such as accessing variables before theyโ€™ve been declared.

You can opt-in to strict-mode by pre-facing your file, or function with use strict at the top of the scope, before any code is declared:

'use strict';
// or
"use strict";

We can test if we can access initializations ahead of time with strict-mode enabled:

"use strict";
function doSomething() {
  foo = 20;
}
doSomething();
console.log(foo); // prints 'ReferenceError: foo is not defined'

Not all functions are hoisted alike

Function are classified as either function declarations, or function expressions. The important difference between the two, when discussing hoisting, is declaration. A declared function will be hoisted, while a function created through an expression will not be hoisted.

console.log(typeof hoistedFunction); // prints 'function'
console.log(typeof unhoistedFunction); // prints 'undefined'

function hoistedFunction() {
  // This function _will_ hoisted, because it is *declared* as a function
}

var unhoistedFunction = function() {
  // This function _will not_ be hoisted because it is declared through an expression of a variable, and therefore will be undefined 
}

Order of hoisting precedence matters

There are two rules you have to keep in mind when working with hoisted functions and variables:

  1. Function declaration takes precedence over variable declarations
  2. Variable assignment takes precedence over expression function
console.log(typeof myVar); // prints 'undefined'
console.log(typeof myFunc); // prints 'function'
var myVar = "foo"; // declaration and initialization
function myFunc(){} // declaration
console.log(typeof myVar); // prints 'string'

We can take a deeper look at the steps during the compilation and execution cycle to see whatโ€™s happening with our declaration and initializations:

function myFunc(){...}
var myVar;
console.log(typeof myVar);
console.log(typeof myFunc);
myVar = "foo";
console.log(typeof myVar);

Classes are not hoisted

Classes in JavaScript are in fact special functions, and just as you can define functions with declaration and expression, the class syntax has the same two components: class expressions and class declarations.

Unlike functions and variables, classes do not get hoisted, either through declaration or expression. You need to declare your class before you can use it.

const p = new Rectangle(); // ReferenceError
console.log(typeof Rectangle); // ReferenceError

class Rectangle {}

Conclusion

Letโ€™s summarise what weโ€™ve learned:

  1. Hoisting is a mechanism that inserts variable and function declaractions into memory ahead of assignment and initialization within the given scope of execution
  2. const, let, function expressions and classes do not get hoisted, and cannot be read or accessed before their declaration
  3. safe-mode prevents sloppy hoisting of initialized undeclared variables onto the global scope

To avoid hoisting confusion and issues in the longterm, itโ€™s better to declare your variables and functions ahead of initialization and access. Youโ€™ll avoid a whole set of potentially nasty bugs and undefined warnings polluting your console.