static.html

— a blog about web development and whatnot by Steve Webster

At work we include a third-party JavaScript library on our pages so we can run A/B and multivariate testing, and this code requires at least jQuery 1.4.0. Unfortunately we have more than a few dark corners of our site that are still dependent on an old (1.2.x) version of jQuery, so I had to find a way to detect the version of jQuery and not invoke the A/B testing code if we didn’t have the appropriate version. This proved to be more complicated than I thought, thanks to the way Firefox (actually SpiderMonkey) handles function declaration in blocks.

Since this code is a library provided by a third party library, I didn’t really want to customise the code to add jQuery version detection as I’d have to do it all over again whenever the library is updated. Also, the library consists of global function declarations interspersed with fragments of immediately executed code that calls those functions, and I didn’t fancy refactoring the fragments just to add version detection.

Never mind, I thought. We include this library, along with the test data relevant to the current page, in a JavaScript bundle file generated at runtime and cached by Squid. I figured I’d simply do the jQuery detection upfront and wrap the entire library in a giant if statement:

if ( hasRequiredJQueryVersion ) {
   // Test data here
   // Library code here
}

This took all of about 5 minutes to implement, and worked just fine in Chrome.

The change needed to go live with the next publish and I didn’t have much time left before we cut the release candidate branch, so I picked some likely browsers to test in addition to Chrome: the flaky duo (Internet Explorer 7 and 8), the mysterious newcomer (Internet Explorer 9) and the unpredictable outsider (Opera 10). The code worked just fine in all of them. WIN!

I was content to skip testing in Firefox and Safari because I’ve found their JavaScript implementations to be mostly rock solid, especially for code as simple as the version comparison code I’d written.

The resulting code was effectively an extension of this:

if ( true ) {
    testFunction();
    function testFunction() {
        alert(‘testFunction called’);
    }
}

In every browser I’ve tested with the exception Firefox, the code above will result in an alert window. However, in Firefox you get the following error in the console:

ReferenceError: testFunction is not defined

If you remove the if statement the code runs just fine in Firefox. Thanks to a process known as function hoisting, the function declaration is processed before any of the code is executed. Firefox clearly doesn’t do this within the block of an if statement, while other browsers do.

I’ve created a simple test case if you want to play around.

ECMA-262 says “no”

It turns out that while this behaviour is unique to SpiderMonkey (Firefox’s JavaScript engine), it's arguably doing the right thing; what I'm trying to do is not allowed according to the ECMA-262 spec. The grammar reference section states that the only constructs allowed within a Block are Statement constructs, and that a FunctionDeclaration isn’t a Statement.

That such code might work in some browsers and not others is even explicitly called out in the spec:

Several widely used implementations of ECMAScript are known to support the use of FunctionDeclaration as a Statement. However there are significant and irreconcilable variations among the implementations in the semantics applied to such FunctionDeclarations. Because of these irreconcilable differences, the use of a FunctionDeclaration as a Statement results in code that is not reliably portable among implementations.

Firefox doesn’t actually issue a warning, which would have made my job tracking this issue down much easier. Moreover, functions declared within a block before being called work just fine, so it's not that function declarations can't by used as statements so much as SpiderMonkey won't bother to hoist them before executing any other code in that block.

Lessons learned

Luckily we caught this error during regression testing, and I was able to quickly refactor the code to just wrap the test data in the version check. The library code doesn’t throw any errors in jQuery 1.2.x when there is no test data defined, so this was a fine compromise for me. All’s well that ends well.

So, what did I learn?

  1. Don’t put function declarations in blocks.

  2. Full cross browser testing is only optional if you don’t care whether your code works or not.

  3. If you’re going to do something slightly unconventional, check the spec first.

  4. jslint is almost always smarter than you.

  5. When you’re in a hurry, you’ll forget all about 2, 3 and 4.