JavaScript is Hard Part 2: The Hidden World of Hoisting


If you are interviewing prospective JavaScript developers and you feel like being a little bit mean then you could do worse than ask this question:

In JavaScript, is it possible to call a function before it has been declared?

It’s easy enough to demonstrate that it is indeed possible, as this self-executing function proves:

(function() {
    console.log(typeof greet);    // prints "function"
    greet();                      // alerts "hello"

    function greet() {
        alert("hello!");
    }
})();

Unfortunately for the hypothetical interview candidate it’s just as easy to demonstrate that it’s not possible:

(function() {
    console.log(typeof greet);    // prints "undefined"
    greet();                      // Error: greet is not a function

    var greet = function() {
        alert("hello!");
    }
})();

In the first case we declared a named function and in the second case we declared a variable and assigned an anonymous function as the value. Although they seem very similar, the first case allows us to call the function before it is declared and the second case does not. This doesn’t seem very intuitive unless you understand the mechanism at work behind the scenes.

Hoisting

Javascript interpreters employ a slightly mysterious process called declaration hoisting, in which all function and variable declarations are moved to the top of their containing scope. If a variable is declared and assigned in one statement then the statement is split into two, with only the declaration getting hoisted. For example, here is a block of code as written by a developer:

(function() {
    console.log(a);     // prints "undefined"
    b();                // prints "b"
    console.log(c);     // prints "undefined"
    d();                // Error: d is not a function
    e();                // Error: e is not a function

    var a = "myValue";
    function b() { console.log("b"); };
    var c;
    var d = function namedFunc() { console.log("d"); };
    var e = function() { console.log("e"); }
})();

And here is how that block of code would be interpreted after the hoisting process has taken place:

(function() {
    // hoisted section
    var a;
    function b() { console.log("b"); };
    var c;
    var d;
    function namedFunc() { console.log("d"); };
    var e;
    function __anon() { console.log("e"); }

    // user code
    console.log(a);     // prints "undefined"
    b();                // prints "b"
    console.log(c);     // prints "undefined"
    d();                // Error: d is not a function
    e();                // Error: e is not a function

    a = "myValue";
    d = namedFunc;
    e = __anon;
})();

Suddenly it makes more sense. Notice that variables and their assignments are split up with only the declaration getting hoisted, while functions declarations are hoisted along with their bodies. That’s why we’re able to call b().

Now that we know how hoisting works we can see why it wasn’t possible to call greet() before it was declared in the second code example. The variable greet would have been hoisted, but the assignment of that variable to the function does not happen until after greet() is called.

Why should we care?

Besides the fact that you can impress friends at parties with your esoteric JavaScript knowledge, you should be aware of hoisting because not knowing about it can lead to nasty bugs, and before you know it you will be hoist by your own petard, whatever that means.

petard

A petard: some kind of medieval bomb apparently

Take this slightly contrived example adapted from Ben Cherry’s blog about hoisting:

var a = "old value";
function changeTheValueOfA() {
	a = "new value";
	return;

	function a() {}
}
changeTheValueOfA();
console.log(a);

If you execute this code you will find that it prints “old value”, indicating that the changeTheValueOfA function doesn’t work. To someone unacquainted with hoisting this may seem bizarre.

The reason it doesn’t work is that the local function a() will be hoisted to the top of changeTheValueOfA, and since declarations are function-scoped the assignment of “new value” will apply to the local a rather than the a outside the function.

This shows another facet of hoisting – all declarations are hoisted whether they are relevant or not, even declarations that are in unreachable code blocks.

Here’s another one:

(function() {
    var a = "initial";
    if(a) {
        function f() { console.log("1"); };
    } else {
        function f() { console.log("2"); };
    }
    f();
})();

Since we are now wise to the tricks of hoisting we know that function f will be hoisted. But in this case there are two functions with the same name. You might expect the first instance of f to be hoisted because a is defined, so the declaration in the if block will execute rather than the else block.

Unfortunately the hoisting process does not care in the slightest about which branch executes, and even more unfortunately there is no defined behaviour regarding which function declaration “wins” by overwriting the other.

If you run this code in Firefox it will print 1. If you run it in Internet Explorer it prints 2. Ouch – try tracking that one down if it was buried in a large code base!

What should we do about all this?

It’s worth pointing out that the last example is actually illegal in ECMAScript, although no browser will complain about it (unless you use the ECMAScript 5 strict mode). Hoisting generally won’t trip you up unless you are already doing something a little bit dubious, such as declaring a variable twice.

The static analysis tool JS Lint will recommend that all variable and function declarations should be placed at the top of their containing scope, in other words write your code as it would be interpreted after hoisting has taken place. In most cases I think this is overkill, just take a bit of care with your declarations and you’ll be fine.

Next time: some esoterica about deleting objects.

This is part two of a series of posts about JavaScript quirks. For part one, click here. For part three, click here.

Related Posts with Thumbnails

One Comment

Leave a Comment