static.html
— a blog about web development and whatnot by Steve Webster
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?
-
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 slightly unconventional, 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.