Friday, April 20, 2007

Comparing Javascript Inheritance in Firefox, IE7 and Opera

When I started writing Javascript programs just a few months ago, I got excited in how you could augment an existing class by modifying its prototype. For example, you could add a functional-style map (which is what Google's map-reduce is based on) to Array by modifying its prototype as follows:
Array.prototype.map = function (f)
{
var len = this.length;
var arr = new Array(len);
for (var i = 0; i < len; i++)
arr[i] = f(this[i]);
return arr;
}
Now, for any array, you could call the map method, passing in a mapping function as an argument, and the result would be a new array where each element is transformed from the original array by the mapping function. For example:
var xa = [1, 2, 3, 4];
var ya = xa.map( function(n) { return n * n; } );
would result in ya = [1, 4, 9, 16].

I also became fascinated by a technique to dynamically making an existing object inherit from another object, by copying the other object's properties over. For example, I don't need to hard-code event handlers in HTML. I could wrap these event handlers in a constructor function along with all local, instance-specific states. I lookup the HTML element whose behavior I want to override, and inject my event handlers to it. I could also reuse that event handling class for many elements in the same document without worrying if I would pollute global variables. This achieves good abstraction and code reuse.

Since all objects are inherited from Object, I could write a function Object.prototype.inherit and make that sort of dynamic inheritance available to all objects. Right?

Not so fast. It turns out that in Internet Explorer 6 and 7, not all objects are instances of Object. Most notable ones are document, window, any values representing DOM node or a node list, and any instances of ActiveXObject. As a result, they do not inherit changes made to Object.prototype. Furthermore, these objects don't have a constructor. All objects have a "constructor" property, but these objects are an exception.

Are there any exceptions like that in Firefox? There is. If you try to obtain a DOM node list, for example, by document.getElementsByTagName("A"), you would get an object whose constructor is HTMLCollection. Firefox admits such object to be an instance of HTMLCollection class, but the object doesn't inherit changes made to HTMLCollection.prototype (which is actually read-only). It does inherit changes to Object.prototype, however.

Such inconsistencies are probably due to the fact these objects are implemented natively (in C) and wrapped for Javascript.

Some browsers are more consistent for native objects than others. All classes (constructor functions) are instances of Function because that is the way Javascript is designed. In Firefox, IE and Opera, all primitive classes such as Number, String, RegExp, even Function itself are instances of Function. The situation is different for DOM classes.
  • Document object is an instance of HTMLDocument (Firefox, Opera), and HTMLDocument is an instance of Function (Opera only).
  • A DOM node list object returned by document.getElementsByTagName is an instance of NodeList (Opera, this is the correct W3C DOM behavior) or HTMLCollection (Firefox, compatible but incorrect), and NodeList is an instance of Function (Opera).
  • XMLHttpRequest is an instance of Function (Opera, IE7).
  • ActiveXObject is an instance of Function (IE6, IE7).
  • IE doesn't define DOM classes such as HTMLDocument, HTMLElement, NodeList, etc.
As one can see, Opera is very consistent in inheritance even for native objects.

Here is what you can count on when it comes to prototype inheritance. In Firefox and Opera, all objects inherit from Object. In IE, objects that interface with the browser or other native components do not inherit from Object.

In the beginning of this article, we saw how prototype inheritance can be used to augment existing classes. If this feature is implemented consistently, then this is a powerful way to extend Javascript. Imagine how we can work-around browser incompatibility by implementing the missing functions in Javascript. For example, DOM2 events can be simulated by setting appropriate HTML element attributes in DOM1. To an extreme, it is possible to emulate DOM in ancient browsers as long as we have document.write (though it is not practical to do).

It is not surprising that IE has the most broken Javascript implementation, considering that IE is historically the dominant browser. Because prototype inheritance is not possible for DOM, browser, and other native objects, IE is the least flexible in terms of language extension and work-around. By contrast, both Firefox and Opera have better prototype inheritance, and it is easier to extend or adapt these browsers.

No comments: