A common technique for writing cross-browser JavaScript code is to detect the features that you wish to use before you actually use them. Good object detection is done on a case-by-case basis, analyzing each feature as it’s encountered.
Some common examples of object detection:
// Is XPath querying available? if ( document.evaluate ) {} // Does the element have style information? if ( element.style ) {} // Can you get the computed style? if ( document.defaultView && document.defaultView.getComputedStyle ) {}
All that these are doing is detecting if these particular properties are available before trying to use them. If you were inclined you could take these a step further and attempt to verify the types of the object properties as well (such as verifying that document.evaluate
is a function).
Now one common problem – and one that’s as old as JavaScript itself – is to use object detection as a means of making sweeping generalizations concerning a set of features or an entire browser.
The classic bad one is:
// Are we in IE? if ( document.all ) {}
The obvious problem, with the above, is that it is capable of returning ‘true’ in browsers that aren’t Internet Explorer (such as Opera). The difficulty arises when the developer assumes that an Internet Explorer-exclusive feature is available based upon the existence of that simple – and completely unrelated – property.
That takes into account the obvious ‘enemy’ – an unknown browser – however there’s a far bigger threat: Unintended consequences introduced by libraries or obscure fringe cases introduced by the user or markup. Let’s look at both.
Feature Introduction by Libraries
Libraries frequently introduce features that are missing from a particular browser. A common one, for example, is introducing document.getElementsByClassName
for browsers that don’t have it. At the same time I’ve seen code like the following:
// Are we using Firefox 3? if ( document.getElementsByClassName ) {}
Which is incredibly problematic. To start with: That feature may be introduced, right now, by a library or other snippet causing unintended consequences in your application. Additionally future browsers will inevitably introduce this feature (such as Safari 3.1) causing this detection to go completely out the window.
Fringe Cases From Global Variables
While they don’t happen often for those that they do happen to they can be a living hell. Specifically when object detection takes place within the global object (e.g. window.opera
) then all bets are off as to its cross-browser applicability. Let’s take a look at an example:
<html> <head> <script> window.onload = function(){ if ( window.opera ) { alert("You're using Opera, pick me!"); } }; </script> </head> <body> <h1>Which browser?</h1> <a href="http://mozilla.com/" id="firefox"> Firefox</a><br/> <a href="http://apple.com/" id="safari"> Safari</a><br/> <a href="http://opera.com/" id="opera"> Opera</a><br/> <a href="http://microsoft.com/" id="ie"> Internet Explorer</a> </body> </html>
You would expect the alert to pop up when the user visits the page in Opera – and it does – however the alert also comes up in Internet Explorer. This is because all elements with an ID are automatically introduced as global variables (in the above there are four globals: firefox, safari, opera, and ie that all reference their associated elements). This is a fantastically difficult bug to spot and work around – thus you should be very cautious when doing any form of object detection on the global object.
Of course, it almost goes without saying, that there could exist an ‘opera’ variable elsewhere in the site that could interfere with this detection. Although that would, arguably, be easier to detect as it would make every browser trigger a false positive.
There’s two morals here:
- Only use object detection to detect exactly what you need to use.
- Be wary of detecting properties on the global object – it’s capable of lying to you.
.mario (February 29, 2008 at 4:34 am)
The alert will also pop up on FF2.0.0.12 and FF3.0b3pre when replacing window.opera by just opera – if no DOCTYPE is set.
Simon Proctor (February 29, 2008 at 4:35 am)
John don’t forget that elements with name attributes can also pollute the global scope in IE. One bug I kept getting in my last job involved one of the designers who gave the submit buttons for forms the name ‘submit’.
Making using Javascript to submit the form more than a little annoying.
Apostolos Tsakpinis (February 29, 2008 at 6:51 am)
Wouldn’t be safe to run those detections on the global namespace of a dynamically created IFrame ?
Jörn Zaefferer (February 29, 2008 at 7:54 am)
An example for Feature Introduction by Libraries was the XMLHttpRequest introduced in earlier versions of jQuery. It broke code that detected IE6 by checking if window.XMLHttpRequest didn’t exist…
Michael Kaply (February 29, 2008 at 8:04 am)
I’m confused. You say to use object detection but then you say:
if ( document.getElementsByClassName ) {}
is bad. Isn’t that object detection?
How else am I supposed to detect I have getElementsByClassName on FF3?
Mislav (February 29, 2008 at 8:38 am)
Michael: dunno … maybe:
document.getElementsByClassName &&
document.getElementsByClassName.toString().indexOf('native code') >= 0
Jörn Zaefferer (February 29, 2008 at 8:52 am)
@Michael: The problem is not the check for getElementsByClassName itself, but to use that to check if the browser is FF3. Just as the check for document.all to detect IE.
Dan G. Switzer, II (February 29, 2008 at 9:39 am)
I wish developers would have fought strong for vendors to put accurate information in the Navigator object. If the Navigator object actually produced accurate information about the browser, resolving these issues would be much easier.
aaron (February 29, 2008 at 9:44 am)
Thanks. I never realized that IE puts ids into the global namespace.
Eber Irigoyen (February 29, 2008 at 10:53 am)
or, you want a dinamic language, that’s exactly what you get J
Leo Petr (February 29, 2008 at 11:03 am)
Dan G: It wouldn’t be, because branching based on browser detection means the developer has to update the page whenever browser capabilities change. It is a fundamentally flawed idea.
Capability detection is both very easy to do and the only future-proof way to develop.
Tom (February 29, 2008 at 11:32 am)
Eber, globals aren’t necessarily inherent in the concept of dynamic typing. JavaScript is just worst than most in this regard (defaulting undeclared variables to the global object). But still, the popular dynamic languages I know well (JavaScript, Ruby, Python) all suffer somewhat (and in their own ways) from easy namespace pollution. It just doesn’t _have_ to be that way.
Giuseppe Raso (February 29, 2008 at 2:37 pm)
You can use the method toString to verify whether window.opera is an object indicating that the browser is Opera:
if(window.opera.toString() == "[object Opera]") {}
Wade Harrell (February 29, 2008 at 5:17 pm)
@Dan: I think it is better to target code that does not care about browser but instead is concerned with functionality. It future proofs you better than looking at Navigator. I know it does not ‘always’ work that way, yet I will try and place my navigator specific edge cases inside my functionality check. Most of the time though, if you want to use specific functionality you can check for it’s existence then use it. When a future version of your least favorite browser of the moment {saf2} decides to support that behavior {saf3} then you don’t have to revisit the code.
Mark Holton (March 1, 2008 at 12:21 pm)
Very nice… been wondering… what is the best way to perform object detection for the presence of E4X?
Nick Fletcher (March 1, 2008 at 8:43 pm)
I was looking through the jQuery code the other day and I noticed a lot of this sort of thing:
if (jQuery.browser.msie) { … }
Is this because object detection isn’t reliable in these cases? Just wondering how this would affect browsers that spoof the UA string.
pd (March 1, 2008 at 9:36 pm)
if object detection is unreliable, how does jQuery differentiate between browsers to provide this utility method:
http://docs.jquery.com/Utilities/jQuery.browser
Nick Fletcher (March 1, 2008 at 10:32 pm)
Nevermind. Just did a search in the jQuery Google Group and learned that I’ve rehashed an age old question. :)
@pd: I was asking if it was unreliable. Also, jQuery.browser sniffs the UA string.
pd (March 2, 2008 at 5:45 am)
The UA string is surely no more accurate than object detection?
Nick Stakenburg (March 2, 2008 at 9:14 pm)
pd: The userAgent string can also give unexpected results. Consider the following:
IE7 can have this in the userAgent string:
‘Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) …’
var userAgent = navigator.userAgent.toLowerCase();
/msie 6/.test(userAgent); //--> true
/msie 7/.test(userAgent); //--> true
This example gets used a lot though. There are better ways to do this with both UA string and Object detection. One moral I would add is that you not only need to know what to look for, but you also need to be aware of the context you might be searching it in.
Jim (March 2, 2008 at 11:54 pm)
Without more info I’d have to disagree that making sweeping generalizations about a given browser is poor strategy. There is only so much time in the day to write for each. Object detection has (at least in the past) been more reliable than using the useragent. Most groups don’t replicate the things we check for….that said any approach to client-side development is only going to work if it’s tested against the browsers your audience is going to probably use. It becomes fairly obvious once you start browsing the web that most groups don’t even do this much. Sometimes the group you’re with even give specific direction NOT to test against the competition which is fairly self defeating. Why bother publishing on the web if you don’t want everyone to read your message?