JavaScript: Hoisting

Variables are a pretty basic component of just about any programming language, and JavaScript is no exception:

var myVariable = 'Hello, there!';

But since JavaScript is... well... JavaScript... there's a quirk here that creates some pretty unusual behavior if you aren't prepared for it. Hoisting is a process that, in a nutshell, moves variables and functions to the top of their scope.

Variables

The lifecycle for a JavaScript variable can be roughly described in three parts: declaration, assignment, and access. All three of these steps should be included.

When a scope is created, JavaScript will scan the top-level for var and function declarations and assigns memory for those variables. In a practical sense, this behavior makes JavaScript parse scopes as if all of the function and var declarations were on the first line of the new scope:

function hoist() {
    a = true;       // assignment
    console.log(a); // access
    var a;          // declaration
}

// is equivalent to

function hoist() {
    var a;          // declaration
    a = true;       // assignment
    console.log(a); // access
}

Like many languages, JavaScript also allows you to perform the declaration and assignment steps on one line. However, they will be split into two steps, regardless — the assignment will not be hoisted:

function hoist() {
    a = true;       // assignment
    console.log(a); // access: true
    var a = false;  // declaration & assignment
    console.log(a); // access: false
}

// is equivalent to

function hoist() {
    var a;          // declaration
    a = true;       // assignment
    console.log(a); // access: true
    a = false;      // assignment
    console.log(a); // access: false
}
Some Caveats
1. This only works with ECMAScript 5 variable declarations.

When working with variables, only those declared with var are affected. Variables declared with let and const are relatively new to JavaScript and follow different rules.

2. You should declare variables before assigning them.

If a variable is assigned before it is declared, it will be implicitly moved to the global scope. This behavior can result in bugs that are difficult to track, especially in a large project.

function createsGlobal() {
    a = true;       // assignment (without declaration)
}
createsGlobal();
console.log(a);     // access: true

// is equivalent to

var a;              // implicit declaration
function createsGlobal() {
    a = true;       // assignment
}
createsGlobal();
console.log(a);     // access: true

You can address this problem by adding 'use strict' or "use strict" to the top of the scope. It is also considered a best-practice to declare variables at the top of a function since this is how JavaScript already interprets your code.

function noGlobals() {
    'use strict';
    b = true;       // ReferenceError: b is not defined
}

function noErrors() {
    var b, c, d;
    b = true;
    c = true;
    d = false;
    return { b, c, d };
}
3. You should declare variables before accessing them.

If a variable is accessed before it is declared, it will throw a reference error.

x; // ReferenceError: x is not defined

You can prevent this by declaring a variable at the most specific scope possible:

function getCounter() {
    var x = 0;
    return {
        increment() { return ++x; },
        decrement() { return --x; },
    };
}

Functions

Function declarations and definitions follow similar rules to variables. Like variables, their declarations are hoisted. Unlike variables, their assignments are also hoisted.

hoisted();
function hoisted() {
    console.log('Hoisted!');
}

// is equivalent to

function hoisted() {
    console.log('Hoisted!');
}
hoisted();
Variables and Functions with the same name

Let's say you find code that looks something like this:

var x = true;
function x() {
    // do stuff
}
console.log(x);

How would the JavaScript engine interpret this? The answer is surprisingly simple: hoisting is done in batches.

First, JavaScript will hoist the variable declarations. If we move both declarations to the top of the scope, our example looks like this:

var x, x; // declarations are moved to the top of the scope
// ---
x = true;
x = function() {
    // do stuff
};
console.log(x);

Since x is declared twice, one declaration can be ignored, which leaves us with this:

var x;
// ---
x = true;
x = function() {
    // do stuff
}
console.log(x);

Second, JavaScript will hoist the function definition. This is what really separates functions from variables. Function definitions are evaluated right after hoisting occurs:

var x;
x = function() { // hoisted
    // do stuff
}
// ---
x = true;
console.log(x); // outputs "true"

This has the rather odd side-effect of functions being overwritten by local variables, regardless of where they were defined in a scope.

One major exception to the rule: if a function is explicitly declared as a variable, then the assignment will not be hoisted.

hoisted();      // TypeError: expression is not a function
var hoisted = function() {
    console.log('Not Hoisted!');
};

// is equivalent to

var hoisted;
hoisted();
hoisted = function() {
    console.log('Not Hoisted!');
};

This is a relatively common practice, so it's important to be aware of the difference between the two styles.

Classes

Classes are a special case, in that they are treated as functions but follow the same rules as a const declaration and assignment — they are not hoisted, and they cannot be reassigned. This, coupled with the lack of Internet Explorer support, means that classes are less common in browser-side JavaScript than in Node.js. Given what has already been covered, these two examples should demonstrate the lack of hoisting on classes:

console.log(typeof NotHoisted); // ReferenceError: NotHoisted is not defined
class NotHoisted {}
class ForwardDeclared {}
console.log(typeof ForwardDeclared); // function

Conclusion

Whenever a new scope is declared, three steps are immediately evaluated:

  1. Variable declarations are moved to the top of the scope.
  2. Function definitions are assigned to variables that share their name.
  3. Code within the scope is executed, including assignments and accesses.

However:

  • Variables declared with const, let or class are not hoisted.
  • Overwriting a function's variable will overwrite the function, even if the function is declared later.
  • Function assignment is only hoisted if it is not explicitly assigned to a variable.

Some habits you should build:

  • Put "use strict" or 'use strict' at the top-level scope to catch subtle bugs.
  • Move var declarations to the top of their scope.
  • Avoid using the same name for a variable and a function.

If you would like to learn more about how scope works in JavaScript — including the const, let, and class declarations — you may be interested in reading the JavaScript: Scope entry! Good luck!

Comments

Back to top