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.
Since we’re dealing with 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 a more complicated version of this:
if ( true ) {
testFunction();
function testFunction() {
alert(‘testFunction called’);
}
}
In every browser I’ve tested with the except 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 doesn’t do this within the block of an if
statement
I’ve created a simple test case if you want to play around.
It turns out that depute this behaviour being confined to SpiderMonkey (Firefox’s JavaScript engine), there’s a good reason for it: it’s 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 issue a warning, and functions declared within a block can still be called provided the function declaration appears before the code that calls it.
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, did I learn:
-
Don’t put function declarations in blocks.
-
Full cross browser testing is only optional if you don’t care whether your code works or not.
-
If you’re going to do something esoteric, check the spec first.
-
jslint is almost always smarter than you
-
When you’re in a hurry, you’ll forget all about 2, 3 and 4.