Hoisting 102 - Examining a Global Context Hoisting Gotcha

In an earlier article we covered Variable Hoisting in JavaScript. At the time, I did not expect to face a hoisting related code bug so quickly. And given the circumstances of the bug, I expect it to be one of the most common hoisting problems, so I wanted to share it.

How do it…

Let’s jump right into the setup. You have a function that is defined in one JavaScript file (this file is expected to load first, but may not). Here is simple cross-browser safe logger function:

function logger() {
    if (window.console) {
        console.log.apply(console, arguments);
    }
}

In another JavaScript file (expected to load later asynchronously), the existence of the first function is checked. This second file creates a temporary dummy function, if the original function does not exist:

if (!logger) {
    function logger () {};
}

The problem can also be triggered directly in a single file:

<script>
function test () {
    alert('failed to hoist');
}

if (! test) {
    function test() {
        alert('hoisted');
    }
}

test();
</script>

Everything loads in the correct order, but for some reason the logger function is the second function. What gives? hoisting

How it works…

Here is what happens. The first file loads as expected and defines the the debugger function. Then the second file loads and the if statement does not execute, but nonetheless, the logger function inside of the if statement is hoisted to the top of the current scope. In this example the execution context is the window and the function declaration is hoisted to the top of the window scope. Additionally, since functions are hoisted completely defined, the empty logger overrides the original function.

This really only becomes a major problem, because we are in the global scope, so hoisting problems are not contained. There are many ways to prevent this, but we’ll cover two. The first (and my favorite) is to write all scripts inside an enclosing function to contain any hoisting to the enclosing functions execution context:

(function() {
    if (!window.logger) {
        window.logger = function() {};
    }
}());

The second option is to just use window as an object literal, as we did inside the enclosing function above:

if (!window.logger) {
    window.logger = function() {};
}

The first is just good practice, for writing all scripts, as it forces you to do the right thing automatically and prevents accidental hoisting. Dont’t let global hoisting issue bite you, never work directly in the global execute context.