Apple Developer Connection
Advanced Search
Member Login Log In | Not a Member? Support

Object Detection

The pace of new browser releases may be slower than it was in the early days, but developers must still confront a bemusing array of browser versions and brands that support some JavaScript features but not others. To combat the problem, scripters commonly provide two or more code branches so that a browser follows an execution path containing statements that it supports. Browser sniffing — the task of inspecting navigator object properties for version information — has become largely unmanageable given the browser version permutations available today. This article presents details on an alternative solution — object detection — that frees JavaScript developers from most of this versioning mess.

Browser Sniffing Woes

The fundamental belief behind branching code for a particular browser brand and/or version (and possibly operating system) is that Version N of Browser Brand X implements a known set of core scripting language and object model features. Scripters frequently extend this belief to mean that Versions N and later of the browser support a particular feature. Unfortunately, these beliefs have several flaws.

Let’s start by looking at the browser version number. The navigator.appVersion property returns a string that starts with a number associated with the “Mozilla” version. In the early graphical browser days, when most commercial browsers were built on the University of Illinois NCSA Mosaic browser engine, the browsers quietly identified themselves to servers (with each server file request) as belonging to the Mozilla family (not to be confused with the modern Mozilla.org browser). Unfortunately for scripters, over time the correlation between the Mozilla family version number and actual browser product version number weakened. IE5 says it’s Mozilla 4.0; Netscape 6 thinks it’s Mozilla 5.0. These days, a script must parse the more detailed navigator.userAgent or navigator.appVersion property to discover precisely which product version is running. Netscape 6 includes new navigator object properties that report the browser product version, but you have to branch your code just to access those properties. No matter what, it takes a lot of string parsing code to establish a set of global variables indicating the browser’s version.

Next, you have the “equals” and “greater than or equals” version conundrum. When scripters started sniffing browsers in earnest for Netscape and IE brands of Version 4 browsers, some scripts simply checked whether the version number (regardless of how it was extracted) equalled 4. When IE5 appeared, the scripts failed to use the IE4 powers because 5 does not equal 4. Conversely, those who thought they would be smart and write their Netscape 4 sniffers to accept versions 4 or later got into trouble when NN6 appeared and their NN4 layer-dependent code broke left and right.

On the one hand, the “equals 4” camp failed to think beyond solving a problem for the current browser. On the other hand, the “greater than or equals 4” group got caught in a rare, but obviously not impossible, case of an object model feature (the layer object) being yanked by the browser’s maker in its drive to adopt the W3C DOM standard. While it’s easy to forecast that there will be a succeeding version of a browser, it’s not easy to predict feature deprecation or removal in future browsers. It should be clear, in retrospect, that both browser sniffing approaches required emergency code repair as new browser versions starting knocking on site doors.

Lastly, what most browser sniffers tend to ignore are scriptable browsers other than IE and NN - like Safari! Safari offers extraordinary W3C DOM scripting facilities; but a typical browser sniffer would relegate it to the “dumb and dumber” category, because string parsing of navigator.userAgent would fail to uncover a well-known brand name or sufficiently high version number.

Simple Object Detection

You can solve many of these problems with a technique called object detection. This is a very broad name for an approach that allows scripts to create execution branches based on whether the current browser supports a desired object, property, or method.

You have probably seen object detection at work already without recognizing how powerful it can be. Netscape Navigator 2 (all OS versions) and IE3/Windows did not support scripting for the image rollover effect because IMG elements were not recognized as objects in the DOM. But subsequent browsers implemented the image object, exposing all IMG elements within a page as an array of image objects belonging to the document object. Therefore, if the browser supported the image object, the document object contained an images array (collection) of image objects on the page. By enclosing image object manipulation statements inside a test for the existence of the document.images array, earlier browsers simply bypassed the nested statements without errors:

if (document.images) {
    // image object manipulations here
}

This syntax works because in non-supporting browsers, the expression document.images evaluates to undefined. An if construction’s conditional expression evaluates to the equivalent of false when the expression evaluates to undefined.

Pay close attention to the elegance of this admittedly simple example. Mouse-related event handlers operate on all scriptable browsers, but only those browsers that manipulate images as objects attempt to act on those objects. This is an excellent example of using scripting to add value to a page for those browsers that support specific functionality.

Detecting Supported Properties

With today’s more complex object models, it’s not uncommon to branch code based on support for an object’s property and/or method. For example, in IE4 and later, the document.body object contains a scrollTop property, which is used to determine a mouse event’s y-axis location on the page (not just within the visible part of the page). In an effort to ensure support for the scrollTop property, you might rush to use the following construction:

// maybe headed for trouble
if (document.body.scrollTop) {
    // statements that work with scrollTop property
}

Problems (script errors) occur, however, when the if construction runs in a browser (such as NN4 or IE3) that does not support the document.body object. In attempting to evaluate the conditional expression, the “two-dot” evaluation results in a script error. To prevent the error, the expression must first test for the existence of the document.body object, and then for the existence of the property:

if (document.body && document.body.scrollTop) {
    // statements that work with scrollTop property
}

The reason this works is that when an && (AND) operator is part of a conditional expression, a failure of the left-most expression short-circuits the entire expression. Thus, if document.body isn’t supported (that is to say, it is undefined), then the entire conditional expression evaluates to the equivalent of false, without attempting to evaluate the second expression.

Detecting Supported Methods

Functions within a page expose themselves as objects (of type function) to the document object model. Similarly, object methods expose themselves as objects (of type object in IE4+/Windows and IE5/Mac; type function in NN3+).

Since object methods (the method name without trailing parentheses) return a value when evaluated, you can easily test for a browser’s support of a method. You can reference a method in a conditional expression, just as if it were a property of an object. For example, to see if the browser supports the document.getElementById() method, you can test for it with the following construction:

if (document.getElementById) {
    // supports the method -- go for it!
}

The only glitches with verifying methods occurs with several earlier browsers, notably NN2, IE3/Windows, and IE/Mac prior to version 5. The problems occur when the method is, in truth, supported by the browser: Testing for the presence of an existing method in conditional expressions causes a script error in IE and a misleading evaluation in NN2. The lowest common denominator of browsers that permit method detection are those that support the W3C DOM in some fashion. In other words, for maximum compatibility, you should perform method testing for methods that belong to the W3C DOM. Because support for various W3C DOM objects and methods varies so widely among browser versions, this testing can help scripts gracefully weave their way through potential minefields.

One Gotcha and Its Workaround

Before you use property checking, you need to be aware of the data types and default values of the properties you’re checking. If the value of a supported property is zero or an empty string, a reference to the property in an if conditional expression evaluates to false, giving the incorrect impression that the property does not exist when, in truth, it does.

This is where a JavaScript core language operator: typeof comes to the rescue. When you precede any expression with this operator, the result is a string name of the data type of the value. For example, if a property you’re testing for always returns a string (whether an empty string or one with some text in it), the conditional expression can look like the following:

if (typeof someObject.someProperty == "string") {
    // property exists, so use it
}

As a kind of lazy alternative, you can test for the presence of a property by seeing if its type is anything other than undefined, as follows:

if (typeof someObject.someProperty != "undefined") {
    // property exists, so use it
}

You’re not completely off the hook with this operator, however. It is missing from the core language in Netscape Navigator 2. Therefore, you should use this technique only within execution branches that have passed tests indicating the browser is something other than NN2 — or assume that no one using that admittedly ancient browser will venture to your site.

Evaluating Your Needs

It is difficult to formulate a template to use for object detection. Each page’s compatibility goals and object detection needs are somewhat different.

Perhaps the most important step in implementing object detection is identifying your needs. For example, if your scripts interact only with dynamic style attributes of elements, the primary object detection task is identifying the applicable syntax for referencing elements: using document.all to pick up stragglers still using IE4 vs. document.getElementById() for IE5+ and NN6 users. A function that receives an element’s ID as an argument can accommodate both syntaxes, while protecting even older browsers from triggering errors:

function myFunc(elemID) {
    var elem = (document.getElementById) ? ¬
    	document.getElementById(elemID) : ((document.all) ? ¬
    	document.all[elemID] : null);
    if (elem) {
        // act on element
    }
}

If you don’t want to repeat so much code in your functions, you can alternatively establish global variable flags for element reference categories at the top of your scripts:

var isW3C = (document.getElementById) ? true : false
var isAll = (document.all) ? true : false

Then use these global variables in your conditional expressions for execution branching, as follows:

function myFunc(elemID) {
    var elem = (isW3C) ? document.getElementById(elemID) : ((isAll) ? ¬
		document.all[elemID] : null);
    if (elem) {
        // act on element
    }
}

Mold your decision branches so that the most likely true condition tests first. For example, with so many users now visiting with at least basic W3C DOM browsers, test for W3C DOM support first. Yes, it’s true that IE5 supports both element reference styles, but the W3C DOM version will include NN6 users and future standards-compatible browsers, as well.

For functions that act as event handlers, multiple object detection routines are needed to handle both the diversity of event models and element reference syntaxes, as shown in the following example borrowed from the article Supporting Three Event Models at Once:

function functionName(evt) {
    evt = (evt) ? evt : ((window.event) ? window.event : "")
    if (evt) {
        var elem
        if (evt.target) {
            elem = (evt.target.nodeType == 3) ? evt.target.parentNode : evt.target
        } else {
            elem = evt.srcElement
        }
        if (elem) {
            // process event here
        }
    }
}

Planning

Designing your scripts around object detection frequently takes more planning than the browser sniffing approach. For one thing, it requires that you have a good grasp of what objects, properties, and methods are supported by your target browsers. The immense size of today’s Microsoft and W3C object models, plus the various levels of support in different operating systems and browser versions complicates the matter. Unless you have a photographic memory, you’ll want to have a reference handy, either in the form of up-to-date JavaScript-related books or a compatibility chart. These references provide the information you need about which objects, properties, and methods to test for.

The more you work with the object models, the more you discover ways to use clues about one feature as an indicator of support for other features. For example, every browser that supports the document.getElementById() method is essentially guaranteed to support the style property of any HTML element. After that, however, support for individual style sheet properties can vary widely. Thus, an execution branch following support for document.getElementById() can assume support for the style property and go right into testing for the presence of an uncommon or non-standard style sheet property (such as the IE-only style.pixelWidth property). But use this “support by association” tactic judiciously and only with sufficient familiarity with current browser features. Otherwise you’ll fall into the same bad habits that plague version sniffers. If you’re unsure about associated support, perform explicit, nested object detection to provide error-free execution paths.

As a result, some of your branching may need to be many levels deep. Fear not. Except in huge scripts that must process tons of data in numerous iterative loops, the performance impact of nested if constructions is negligible.

As you work on the code, you need to visualize execution paths that encounter your detection branches. Be sure to visualize not only how execution flows when browsers support your cool features, but also what happens when detection finds no support for your features. Will your scripts degrade gracefully in older browsers?

Testing

While the chief benefits of using object detection are freedom from browser version concerns and a hedge against the unknown (alternative browsers and future browser versions), you are not liberated from the job of testing your code on as many browsers as you can. Just because a browser indicates that it supports a particular object or property doesn’t mean that the support is identical across all supporting browsers.

When Browser Sniffing is OK

You may occasionally encounter problems whose solution requires the old-fashioned browser version sniffing. In general these problems are limited to known bugs or “alternative behavior” of an object in a browser version. For example, if MegaBrowser 3.05 for Windows always reports that an otherwise well-supported object property has a value of zero no matter what the rendering shows and generates an error if you try to set the property value, you’ll have to code a detour for that browser version. You’ll be back to the old navigator.userAgent property parsing to protect users of that browser from its foibles. You can’t hope to anticipate every browser bug, but object detection won’t help you there either.

Some page designers also prefer to use OS-specific style sheets or content, rather than trust one size to fit all. In such cases, it’s perfectly normal and appropriate to branch according to OS version. The following script statements would be in the HEAD portion of a document to link a Mac-specific style sheet definition file for Macintosh clients (and hope for the best on Windows, Unix, and other OSes):

var isMac = navigator.userAgent.indexOf("Mac") != -1
if (isMac) {
    document.write("<link rel='stylesheet' type='text/css' HREF='/css/mac.css'>")
} else {
    document.write("<link rel='stylesheet' type='text/css' HREF='/css/generic.css'>")
}

Worth the Effort?

Until you get comfortable with the planning and thought processes needed to implement object detection, you may wonder if so much extra up-front work will pay off. If you have been through revision cycles in the past as new browsers disrupt your existing scripts, you can begin to appreciate how one of the goals of this technique — less maintenance going forward — will ease your load. Of course, you probably won’t know that your hard work early on saved you from headaches down the road. That makes it a thankless job in a way, but one that, rather than making you look good in the future, can prevent you from looking bad.

Using object detection also leads to good programming practice. You’ll need to know the resources at your disposal and visualize execution paths for when things work and when they don’t. You also grow to appreciate the importance of developing code that anticipates errors and handles them gracefully. Once you grow comfortable with object detection, you’ll wonder why you bothered with convoluted sniffing matrices in the past.