Browser Capability Detection with JavaScript

Professional JavaScript for Web Developers, 3rd EditionAlthough browser vendors have made a concerted effort to implement common interfaces, the fact remains that each browser presents its own capabilities and flaws. Browsers that are available cross-platform often have different issues, even though they are technically the same version. These differences force web developers to either design for the lowest common denominator or, more commonly, use various methods of client detection to work with or around limitations.

Client detection remains one of the most controversial topics in web development. The idea that browsers should support a common set of functionality pervades most conversations on the topic. In an ideal world, this would be the case. In reality, however, there are enough browser differences and quirks that client detection becomes not just an afterthought but also a vital part of the development strategy.

There are several approaches to determine the web client being used, and each has advantages and disadvantages. It’s important to understand that client detection should be the very last step in solving a problem; whenever a more common solution is available, that solution should be used. Design for the most common solution first and then augment it with browser-specific solutions later.

Capability Detection

The most commonly used and widely accepted form of client detection is called capability detection. Capability detection (also called feature detection) aims not to identify a specific browser being used but rather to identify the browser’s capabilities. This approach presumes that specific browser knowledge is unnecessary and that the solution may be found by determining if the capability in question actually exists. The basic pattern for capability detection is as follows:

if (object.propertyInQuestion){
//use object.propertyInQuestion
}

For example, the DOM method document.getElementById() didn’t exist in Internet Explorer prior to version 5. This method simply didn’t exist in earlier versions, although the same functionality could be achieved using the nonstandard document.all property. This led to a capability detection fork such as the following:

function getElement(id){
if (document.getElementById){
return document.getElementById(id);
} else if (document.all){
return document.all[id];
} else {
throw new Error("No way to retrieve element!");
}
}

The purpose of the getElement() function is to return an element with the given ID. Since document.getElementById() is the standard way of achieving this, it is tested for first. If the function exists (it isn’t undefined), then it is used. Otherwise, a check is done to determine if document.all is available, and if so, that is used. If neither method is available (which is highly unlikely), an error is thrown to indicate that the function won’t work.

There are two important concepts to understand in capability detection. As just mentioned, the most common way to achieve the result should be tested for first. In the previous example, this meant testing for document.getElementById() before document.all. Testing for the most common solution ensures optimal code execution by avoiding multiple-condition testing in the common case.

The second important concept is that you must test for exactly what you want to use. Just because one capability exists doesn’t necessarily mean another exists. Consider the following example:

function getWindowWidth(){
if (document.all){ //assumes IE
return document.documentElement.clientWidth; //INCORRECT USAGE!!!
} else {
return window.innerWidth;
}
}

This example shows an incorrect usage of capability detection. The getWindowWidth() function first checks to see if document.all exists. It does, so the function then returns document.documentElement.clientWidth. As discussed in Chapter 8, Internet Explorer 8 and earlier versions do not support the window.innerWidth property. The problem in this code is that a test for document.all does not necessarily indicate that the browser is Internet Explorer. It could, in fact, be an early version of Opera, which supported document.all and window.innerWidth.

Safer Capability Detection

Capability detection is most effective when you verify not just that the feature is present but also that the feature is likely to behave in an appropriate manner. The examples in the previous section rely on type coercion of the tested object member to make a determination as to its presence. While this tells you about the presence of the object member, there is no indication if the member is the one you’re expecting. Consider the following function that tries to determine if an object is sortable:

//AVOID! Incorrect capability detection - only checks for existence
function isSortable(object){
return !!object.sort;
}

This function attempts to determine that an object can be sorted by checking for the presence of the sort() method. The problem is that any object with a sort property will also return true:

var result = isSortable({ sort: true });

Simply testing for the existence of a property doesn’t definitively indicate that the object in question is sortable. The better approach is to check that sort is actually a function:

//Better - checks if sort is a function
function isSortable(object){
 return typeof object.sort == "function";
}

The typeof operator is used in this code to determine that sort is actually a function and therefore can be called to sort the data contained within.

Capability detection using typeof is preferred whenever possible, but it is not infallible. In particular, host objects are under no obligation to return rational values for typeof. The most egregious example of this occurs with Internet Explorer. In most browsers, the following code returns true if document.createElement() is present:

//doesn't work properly in Internet Explorer <= 8
function hasCreateElement(){
return typeof document.createElement == "function";
}

In Internet Explorer 8 and earlier, the function returns false because typeof document.createElement returns "object" instead of "function". As mentioned previously, DOM objects are host objects, and host objects are implemented via COM instead of JScript in Internet Explorer 8 and earlier. As such, the actual function document.createElement() is implemented as a COM object and typeof then returns "object". Internet Explorer 9 correctly returns "function" for DOM methods.

Internet Explorer has further examples where using typeof doesn’t behave as expected. ActiveX objects (supported only in Internet Explorer) act very differently than other objects. For instance, testing for a property without using typeof may cause an error, as in this code:

//causes an error in Internet Explorer
var xhr = new ActiveXObject("Microsoft.XMLHttp");
if (xhr.open){ //error occurs here
//do something
}

Simply accessing a function as a property, which this example does, causes a JavaScript error. It is safer to use typeof; however, Internet Explorer returns "unknown" for typeof xhr.open. That means the most complete way to test for the existence of a function on any object in a browser environment is along the lines of this function:

//credit: Peter Michaux
function isHostMethod(object, property) {
var t = typeof object[property];
return t=='function' ||
(!!(t=='object' && object[property])) ||
t=='unknown';
}

You can then use this function as follows:

result = isHostMethod(xhr, "open"); //true
result = isHostMethod(xhr, "foo"); //false

The isHostMethod() function is the safest to use today, understanding the quirks of browsers. Note that host objects are under no obligation to maintain their current implementation details or to mimic already-existing host object behavior. Because of this, there is no guarantee that this function, or any other, will continue to be accurate if implementations change. As the developer, you must assess your risk tolerance based on the functionality you’re trying to implement.

For an exhaustive discussion of the ins and outs of capability detection in JavaScript, please see Peter Michaux’s article “Feature Detection: State of the Art Browser Scripting” at http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting.

Capability Detection Is Not Browser Detection

Detecting a particular capability or set of capabilities does not necessarily indicate the browser in use. The following “browser detection” code, or something similar, can be found on numerous web sites and is an example of improper capability detection:

//AVOID! Not specific enough
var isFirefox = !!(navigator.vendor && navigator.vendorSub);
//AVOID! Makes too many assumptions
var isIE = !!(document.all && document.uniqueID);

This code represents a classic misuse of capability detection. In the past, Firefox could be determined by checking for navigator.vendor and navigator.vendorSub, but then Safari came along and implemented the same properties, meaning this code would give a false positive. To detect Internet Explorer, the code checks for the presence of document.all and document.uniqueID. This assumes that both of these properties will continue to exist in future versions of IE and won’t ever be implemented by any other browser. Both checks use a double NOT operator to produce a Boolean result (which is more optimal to store and access).

It is appropriate, however, to group capabilities together into classes of browsers. If you know that your application needs to use specific browser functionality, it may be useful to do detection for all of the capabilities once rather than doing it repeatedly. Consider this example:

//determine if the browser has Netscape-style plugins
var hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
//determine if the browser has basic DOM Level 1 capabilities
var hasDOM1 = !!(document.getElementById && document.createElement &&
document.getElementsByTagName);

Download this code CapabilitiesDetectionExample01.htm

In this example, two detections are done: one to see if the browser supports Netscape-style plug-ins, and one to determine if the browser supports basic DOM Level 1 capabilities. These Boolean values can later be queried, and it will take less time than it would to retest the capabilities.

Capability detection should be used only to determine the next step in a solution, not as a flag indicating a particular browser is being used.

This article is excerpted from the Wrox book Professional JavaScript for Web Developers 3rd Edition (copyright 2012 John Wiley & Sons, ISBN: 978-1-1180-2669-4) by Nicholas C. Zakas. NICHOLAS C. ZAKAS has been working with the web for over a decade. During that time, he has worked both on corporate intranet applications used by some of the largest companies in the world and on large-scale consumer websites such as My Yahoo! and the Yahoo! homepage. As a presentation architect at Yahoo!, Nicholas guided front-end development and standards for some of the most-visited websites in the world. Nicholas is an established speaker and regularly gives talks at companies, conferences, and meetups regarding front-end best practices and new technology. He has authored several books, including Professional Ajax and High Performance JavaScript, and writes regularly on his blog at http://www.nczonline.net/. Nicholas’s Twitter handle is @slicknet.

Tags:

Comments

Leave a Reply

What is 11 + 13 ?
Please leave these two fields as-is:
IMPORTANT! To be able to proceed, you need to solve the following simple math (so we know that you are a human) :-)