Quiz

Explain the concept of "hoisting" in JavaScript

Topics
JavaScript
Edit on GitHub

TL;DR

  • Variable declarations (var): Declarations are hoisted, but not initializations. The value of the variable is undefined if accessed before initialization.
  • Variable declarations (let and const): Declarations are hoisted, but not initialized. Accessing them results in ReferenceError until the actual declaration is encountered.
  • Function expressions (var): Declarations are hoisted, but not initializations. The value of the variable is undefined if accessed before initialization.
  • Function declarations (function): Both declaration and definition are fully hoisted.
  • Class declarations (class): Declarations are hoisted, but not initialized. Accessing them results in ReferenceError until the actual declaration is encountered.
  • Import declarations (import): Declarations are hoisted, and side effects of importing the module are executed before the rest of the code.

The following behavior summarizes the result of accessing the variables before they are declared.

DeclarationAccessing before declaration
var fooundefined
let fooReferenceError
const fooReferenceError
class FooReferenceError
var foo = function() { ... }undefined
function foo() { ... }Normal
importNormal

Hoisting

Hoisting is a term used to explain the behavior of variable declarations in your code. Variables declared or initialized with the var keyword will have their declaration "moved" up to the top of their containing scope during compilation, which we refer to as hoisting.

Only the declaration is hoisted, the initialization/assignment (if there is one), will stay where it is. Note that the declaration is not actually moved – the JavaScript engine parses the declarations during compilation and becomes aware of variables and their scopes, but it is easier to understand this behavior by visualizing the declarations as being "hoisted" to the top of their scope.

Let's explain with a few code samples. Note that the code for these examples should be executed within a module scope instead of being entered line by line into a REPL like the browser console.

Hoisting of variables declared using var

Hoisting is seen in action here as even though foo is declared and initialized after the first console.log(), the first console.log() prints the value of foo as undefined.

console.log(foo); // undefined
var foo = 1;
console.log(foo); // 1

You can visualize the code as:

var foo;
console.log(foo); // undefined
foo = 1;
console.log(foo); // 1

Hoisting of variables declared using let, const, and class

Variables declared via let, const, and class are hoisted as well. However, unlike var and function, they are not initialized and accessing them before the declaration will result in a ReferenceError exception. The variable is in a "temporal dead zone" from the start of the block until the declaration is processed.

y; // ReferenceError: Cannot access 'y' before initialization
let y = 'local';
z; // ReferenceError: Cannot access 'z' before initialization
const z = 'local';
Foo; // ReferenceError: Cannot access 'Foo' before initialization
class Foo {
constructor() {}
}

Hoisting of function expressions

Function expressions are functions written in the form of variable declarations. Since they are also declared using var, only the variable declaration is hoisted.

console.log(bar); // undefined
bar(); // Uncaught TypeError: bar is not a function
var bar = function () {
console.log('BARRRR');
};

Hoisting of function declarations

Function declarations use the function keyword. Unlike function expressions, function declarations have both the declaration and definition hoisted, thus they can be called even before they are declared.

console.log(foo); // [Function: foo]
foo(); // 'FOOOOO'
function foo() {
console.log('FOOOOO');
}

The same applies to generators (function*), async functions (async function), and async function generators (async function*).

Hoisting of import statements

Import declarations are hoisted. The identifiers the imports introduce are available in the entire module scope, and their side effects are produced before the rest of the module's code runs.

foo.doSomething(); // Works normally.
import foo from './modules/foo';

Under the hood

In reality, JavaScript creates all variables in the current scope before it even tries to executes the code. Variables created using var keyword will have the value of undefined where variables created using let and const keywords will be marked as <value unavailable>. Thus, accessing them will cause a ReferenceError preventing you to access them before initialization.

In ECMAScript specifications let and const declarations are explained as below:

The variables are created when their containing Environment Record is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated.

However, this statement is a litle bit different for the var keyword:

Var variables are created when their containing Environment Record is instantiated and are initialized to undefined when created.

Modern practices

In practice, modern code bases avoid using var and use let and const exclusively. It is recommended to declare and initialize your variables and import statements at the top of the containing scope/module to eliminate the mental overhead of tracking when a variable can be used.

ESLint is a static code analyzer that can find violations of such cases with the following rules:

  • no-use-before-define: This rule will warn when it encounters a reference to an identifier that has not yet been declared.
  • no-undef: This rule will warn when it encounters a reference to an identifier that has not yet been declared.

Further reading

Edit on GitHub