Perfection kills

Exploring Javascript by example

Archives Posts

How ECMAScript 5 still does not allow to subclass an array

July 15th, 2010 by kangax

Subclassing an array in Javascript has never been a trivial task. At least for a certain meaning of “subclassing an array”. Curiously, new edition of the language — ECMAScript 5 — still does not allow to fully subclass an array.

Not everything is lost though, and there are few ways ECMAScript 5 makes this task closer to the ideal. However, there are few fundamental issues which prevent true array subclassing from happening.

Let’s talk about that.

Today we’ll take a look at what it means to subclass an array, what some of the existing implementations/workarounds are, and which drawbacks those implementations have; We’ll see what ECMAScript 5 brings to the table, and what those fundamental issues with subclassing are. We’ll also talk about alternative approaches to subclassing an array, such as using wrappers, and get to know their limitations.

But first, what does it mean to subclass an array? And why do we even need it?

Why subclass an array?

We can define “subclassing an array” as the process of creating an object which inherits from native Array object (has Array.prototype in its prototype chain), and follows behavior similar (or identical) to native array.

The last point about behavior similar to native array is actually very important, as we’ll see later on. Having “subclass” of array could be thought of as being able to create an array object, but an object which would inherit not directly from Array, but from another object, and only then from Array.

In other words, we want behavior similar to this:

var sub = new SubArray(1, 2, 3);
sub; // [1, 2, 3]
 
sub.length; // 3
sub[1]; // 2
 
sub.push(4);
sub; // [1, 2, 3, 4]
 
// etc.
 
sub intanceof SubArray; // true
sub intanceof Array; // true

Note how SubArray constructor creates a sub object identical in its behavior to array (object has “length” property, numeric “0”, “1”, “2” properties, and inherits Array.prototype.* methods). At the same time, it is SubArray that a sub object directly inherits from, not Array.

So what exactly is the purpose of doing all this? Why subclass an array in such way?

There are usually two reasons:

1. Avoid pollution of global Array

Javascript prototypal nature makes it easy to extend all array objects with custom methods. Instead of assigning to direct properties of array objects, it’s much easier and more efficient to assign to array’s “prototype” object (the one that’s usually accessed via Array.prototype).

Array.prototype.last = function () {
  return this[this.length-1];
};
// ...
[1, 2, 3].last(); // 3

However, extending Array.prototype comes with the price; And that price is chance of collisions. When scripts coexist with other scripts in an application, it’s important for those scripts not to conflict with each other. Extending Array.prototype, while tempting and seemingly useful, unfortunately isn’t very safe in a diverse environment. Different scripts can end up defining same-named methods, but with different behavior. Such scenario often leads to inconsistent behavior and hard-to-track errors.

Collisions can happen not only with user-defined code, but also with proprietary methods implemented by environment itself (e.g. Array.prototype.indexOf from JavaScript 1.6, before it was standardized by ES5) or from future standards (e.g. Array.prototype.map, Array.prototype.reduce, etc. — now all part of ES5).

Using constructor function other than native Array — but with same behavior — would allow to avoid such collisions. Instead of extending Array.prototype, another object would be extended (say, SubArray.prototype) and then used to initialize (sub)array objects. Any third party code which depends on methods from Array.prototype would still be able to safely use them.

2. Create data structures naturally inheriting from array

Another reason to subclass an array is to be able to create data structures, which naturally inherit from array; such as Stack, List, Queue, Set, etc. While there are certainly valid use cases for these structures, in this article I will instead focus on the first aspect — reducing chance of collisions. It is somewhat more relevant in context of cross-browser scripting.

Naive approach

Creating objects that inherit from other objects is more or less straightforward in Javascript. We can use well-known clone method:

function clone(obj) {
  function F() { }
  F.prototype = obj;
  return new F();
}

and then set-up inheritance like this:

function Child() { }
Child.prototype = clone(Parent.prototype);

clone might look confusing, but all it does is create an object with another object as nearest ancestor in its prototype chain. It uses intermediate function to avoid executing “parent” constructor. In this example, new Child creates an object with Child.prototype as first object in the prototype chain, Parent.prototype — second, and so on. To visualize, the prototype chain here looks like this:

new Child()
    |
    | [[Prototype]]
    |
    v
Child.prototype
    |
    | [[Prototype]]
    |
    v
Parent.prototype
    |
    | [[Prototype]]
    |
    v
Object.prototype
    |
    | [[Prototype]]
    |
    v
   null

Using clone method is exactly what person attempts when trying to subclass an array for the first time:

function SubArray() {
  // Take any arguments passed to constructor and add them to an instance
  this.push.apply(this, arguments);
}
SubArray.prototype = clone(Array.prototype);
 
var sub = new SubArray(1, 2, 3);

The approach seems reasonable. After all, the goal is to create an object that inherits from Array, so there’s no reason tried-and-true clone wouldn’t work. Or is there? As with few other things in Javascript, it’s not as trivial as it seems.

Problems with naive approach

So what exactly is wrong with subclassing array using clone method? Let’s take a look at how previously declared SubArray function behaves. We’ll be using native array object alongside, for comparison.

var arr = new Array(1, 2, 3);
var sub = new SubArray(1, 2, 3);
 
arr.length; // 3
sub.length; // 0 (in IE<8)
 
arr.length = 2;
sub.length = 2;
 
arr; // [1, 2]
sub; // [1, 2, 3]
 
arr[10] = 'foo';
sub[10] = 'foo';
 
arr.length; // 11
sub.length; // 2

There’s clearly some kind of inconsistency here. Even not counting a bug in IE<8. But what is this strange relation between length and numeric properties in array? And why doesn’t subclassed array behave identical? To understand this, we need to look into what array objects in Javascript really are.

Special nature of arrays

It turns out that arrays in Javascript are almost like plain Object objects, except for one little difference in behavior. The crux of this difference is summarized concisely in one paragraph of specification (15.4):

Array objects give special treatment to a certain class of property names. A property name P (in the form of a string value) is an array index if and only if ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal to 2^32 – 1. Every Array object has a length property whose value is always a nonnegative integer less than 2^32. The value of the length property is numerically greater than the name of every property whose name is an array index; whenever a property of an Array object is created or changed, other properties are adjusted as necessary to maintain this invariant. Specifically, whenever a property is added whose name is an array index, the length property is changed, if necessary, to be one more than the numeric value of that array index; and whenever the length property is changed, every property whose name is an array index whose value is not smaller than the new length is automatically deleted. This constraint applies only to properties of the Array object itself and is unaffected by length or array index properties that may be inherited from its prototype.

For those allergic to the condensed language of ECMA-262, here’s a short summary.

Array objects treat “numeric” properties in a special way. Whenever such property changes, value of array’s “length” property is adjusted as well; it’s adjusted in such was as to make sure that it is always one more than the greatest numeric (own) property of an array. Similarly, when “length” property is changed, numeric properties are adjusted accordingly (but only those that are larger than value of “length”).

We have already seen relation between numeric properties and length in the previous example, but let’s take a look at it again, step by step:

1) When array object is created, its “length” property is set to a value one more than the largest index of an array.

  var arr = ['x', 'y', 'z'];
  arr.length; // 3 (1 greater than largest index of an array — 2 in this case)
 
  arr = ['foo'];
  arr.length; // 1 (1 greater than largest index of an array — 0 in this case)

2) When numeric properties change, so does “length” change — to maintain the relationship of being 1 greater than the largest index.

var arr = ['x', 'y'];
arr.length; // 2, as expected
 
arr[2] = 'z'; // add another numeric property (2) larger than the largest existing one (1)
arr.length; // 3 — length is changed to be 1 greater than (new) largest index (2)

3) When “length” property changes, numeric properties are adjusted in such way so that greatest index is 1 smaller than value of “length”.

var arr = ['x', 'y', 'z'];
arr.length = 2;
 
arr; // ['x', 'y'] — note how last element (z) is deleted, because being at 2nd index, 
     //              it doesn't satisfy criteria of largest index being 1 less than length
 
arr.length = 4;
 
arr; // ['x', 'y'] — "increasing" length doesn't affect numeric properties...
 
arr.join(); // "x,y,," ...but has consequences visible in other cases, such as when using `Array.prototype.join`
 
arr.push('z');
arr; // ['x', 'y', undefined, undefined, 'z'] — ...or when using `Array.prototype.push`

Now you know the “special” nature of Array objects in Javascript, which is in the relationship between “length” and numeric properties. One little detail we haven’t looked at is that array’s “length” property MUST always have a value of non-negative integer less than 2^32. Whenever this condition is violated, a RangeError is thrown:

var arr = [];
arr.length = Math.pow(2, 32); // RangeError
 
arr.length; // 0 (length is still 0, as it initially was)
 
arr.length = Math.pow(2, 32) - 1; // set length to maximum allowed value
 
arr.length++; // RangeError (when setting length explicitly)
arr.push(1); // RangeError (or when setting length implicitly)

Function objects and [[Construct]]

It should start to make sense why there are discrepancies in behavior of objects created via SubArray and Array functions. Even though SubArray creates an object that inherits from Array.prototype, that object completely lacks array’s special behavior. The SubArray instance is nothing more than a plain Object object (as if it was created via an object literal — { }).

But why does SubArray create an Object object and not an Array object? The core of this issue is in the way functions work in ECMAScript.

When new operator is applied to an object — as in new SubArray — that object’s internal [[Construct]] method is called. In our case, it is [[Construct]] of SubArray function. SubArray — being a native function — has [[Construct]] that’s specified to create a plain Object object, and invoke corresponding function providing newly created object as this value. Any native function, including SubArray, should create an Object object and return it as a result.

Now it’s worth mentioning that it’s possible to sort of supersede return value of [[Construct]] by explicitly returning non-primitive value from constructor function:

function SubArray() {
  this.push.apply(this, arguments);
  return []; // return array object explicitly
}

— but in that case, returned object does NOT inherit from constructor’s “prototype” (SubArray.prototype in this case); neither is constructor function invoked with that object as this value:

var sub = new SubArray(1, 2, 3);
 
// Object doesn't have 1, 2, 3, as constructor was never called with `this` value referencing returned object
sub; // []
 
// SubArray is not in the prototype chain of returned object
sub instanceof SubArray; // false

As you can see, creating an object that inherits from Array.prototype is only part of the story. The biggest issue is to preserve the special relation of length and numeric properties. This is why using regular clone approach is not quite up to the task.

The importance of array special behavior

A reasonable question at this point is — “Why does array special behavior matter”? Why would we want to preserve relationship between length and numeric properties when subclassing an array? It turns out that consequences of proper length are not only visible when working with length directly, but also indirectly, when performing other tasks via Array.prototype.* methods.

Take for example Array.prototype.push — a method to append items to the end of array. To determine from which position to start inserting elements into, push retrieves a value of array’s “length”. If length is not preserved properly, elements are inserted at the wrong location:

var arr = ['x', 'y'];
arr.length = 5;
arr.push('z'); // 'z' is inserted at 5th index, since that is what the value of "length" is
arr; // ['x', 'y', undefined, undefined, undefined, 'z']

Take another method — Array.prototype.join. Used to return a representation of an array by concatenating all elements with a separator, Array.prototype.join also uses length property to determine when to stop concatenating values:

var arr = ['x', 'y'];
arr.join(); // "x,y"
arr.length = 5;
arr.join(); // "x,y,,,"

Same goes for Array.prototype.concat — method used to produce a new array by concatenating values passed to concat:

var arr = ['x'];
arr.length = 3;
arr.concat('y'); // ['x', undefined, undefined, 'y']

Finally, the special behavior is often cleverly exploited in other situations, such as to “clear” an array (i.e. delete all of its numeric properties):

var arr = [1, 2, 3];
arr.length = 0;
arr; // [] — setting length to 0 effectively removes all numeric properties (elements) of an array

Existing solutions

Now that we’re familiar with the theory, let’s see what the situation is with subclassing arrays in practice. There have been few attempts in the past, with various levels of “success”. Here are a couple of most popular ones:

Andrea Giammarchi solution

One of the recent implementations is Stack, by Andrea Giammarchi, which looks like this:

var Stack = (function(){ // (C) Andrea Giammarchi - Mit Style License
 
  function Stack(length) {
    if (arguments.length === 1 && typeof length === "number") {
      this.length = -1 < length && length === length << 1 >> 1 ? length : this.push(length);
    }
    else if (arguments.length) {
      this.push.apply(this, arguments);
    }
  };
 
  function Array() { };
  Array.prototype = [];
 
  Stack.prototype = new Array;
  Stack.prototype.length = 0;
  Stack.prototype.toString = function () {
    return this.slice(0).toString();
  };  
 
  Stack.prototype.constructor = Stack;
  return Stack;
})();

It’s an interesting solution, which mainly works around IE<8 bug with Array.prototype.push and length property. However, as should be obvious by now, it doesn’t really solve the problem of maintaining relation between length and numeric properties:

var stack = new Stack('x', 'y');
stack.length;           // 2
 
// so far so good
 
stack.push('z');
stack.length;           // 3
 
// still good
 
stack[3] = 'foo';
stack.length;           // 3
 
// not good anymore (length should have been changed to 4)
 
stack.length = 2;
stack[2];               // 'z'
 
// still not good (element at 2nd index should have been deleted)

Dean Edwards solution

Another popular solution is by Dean Edwards. This one takes a completely different approach — instead of creating an object that inherits from Array.prototype, an actual Array constructor is “borrowed” from the context of another iframe.

// create an <iframe>
var iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
 
// write a script into the &lt;iframe> and steal its Array object
frames[frames.length - 1].document.write(
  "<script>parent.Array2 = Array;<\/script>";
);

The reason this “works” is due to browsers creating separate execution environments for each frame in a document. Each such environment has a separate set of both — built-in and host objects. Built-in objects include global Array constructor, among others. Array object of one iframe is different from Array object of another iframe. They also don’t have any kind of hierarchical relation:

// assuming that SubArray was borrowed from another iframe
 
var sub = new SubArray(1, 2, 3);
 
sub instanceof SubArray; // true
sub instanceof Array; // false
sub instanceof Object; // false

Notice how sub is reported as NOT an instance of Array, and NOT an instance of Object. This is because neither Array, nor Object are anywhere in the prototype chain of sub object. Instead, prototype chain consists of SubArray.prototype, followed by <Object from another iframe>.prototype:

new SubArray()
    |
    | [[Prototype]]
    |
    v
<another iframe>.Array.prototype
    |
    | [[Prototype]]
    |
    v
<another iframe>.Object.prototype
    |
    | [[Prototype]]
    |
    v
   null

This brings us to one “consideration” with this approach — difficulties determining the nature of an object derived from such iframe. It’s no longer possible to determine that an object is an array using instanceof or constructor checks [1]:

  // is this object an array?
 
  sub instanceof Array; // false
  sub.constructor === Array; // false

It is, however, still possible to use [[Class]] check (we’ll talk about [[Class]] later on):

  Object.prototype.toString.call(sub) === '[object Array]'; // true

Another, more inherent, downside of this approach is that it doesn’t work in non-browser environments (or, more precisely, in any environment without support for iframes). This problem is likely to become even bigger, given that server-side Javascript implementations are rising quite fast.

Finally, it was reported that Array borrowing can cause mixed content warning in IE6, among few other minor issues.

Other than that, iframe-based array “subclassing” is free of downsides of solutions like Stack, since we’re dealing with real array objects, and so proper length/indices relation.

ECMAScript 5 accessors to the rescue

But let’s talk about ECMAScript 5, which as I mentioned in the beginning, brings something that helps with subclassing arrays. This “something” is actually nothing but property accessors. These useful language constructs have been present in some popular implementations (SpiderMonkey, JavaScriptCore, and others) as a non-standard extension for quite a while now. They are now standardized by the new edition of the language.

Using accessors, it’s rather trivial to create an Object object with special length/indices relation — relation that’s identical to that of Array objects! And since we already know how to create an object with Array.prototype in its prototype chain, combining these two aspects would allow for a complete emulation of arrays.

There’s one little detail about implementation. Since ECMAScript (including last, 5th version) doesn’t provide any catch-all (aka __noSuchMethod__) mechanism, it’s not possible to change value of length property of an object when numeric property is modified; in other words, we can’t intercept the moment when ‘0’, ‘1’, ‘2’, ‘15’, etc. properties are being set. However, accessors allow us to intercept any read access of length property and return proper value, depending on which numeric properties object has at that moment. This is all we really need.

Here’s an implementation of it, at about 45 lines of code:

var makeSubArray = (function(){
 
  var MAX_SIGNED_INT_VALUE = Math.pow(2, 32) - 1,
      hasOwnProperty = Object.prototype.hasOwnProperty;
 
  function ToUint32(value) {
    return value >>> 0;
  }
 
  function getMaxIndexProperty(object) {
    var maxIndex = -1, isValidProperty;
 
    for (var prop in object) {
 
      isValidProperty = (
        String(ToUint32(prop)) === prop && 
        ToUint32(prop) !== MAX_SIGNED_INT_VALUE && 
        hasOwnProperty.call(object, prop));
 
      if (isValidProperty && prop > maxIndex) {
        maxIndex = prop;
      }
    }
    return maxIndex;
  }
 
  return function(methods) {
    var length = 0;
    methods = methods || { };
 
    methods.length = {
      get: function() {
        var maxIndexProperty = +getMaxIndexProperty(this);
        return Math.max(length, maxIndexProperty + 1);
      },
      set: function(value) {
        var constrainedValue = ToUint32(value);
        if (constrainedValue !== +value) {
          throw new RangeError();
        }
        for (var i = constrainedValue, len = this.length; i < len; i++) {
          delete this[i];
        }
        length = constrainedValue;
      }
    };
    methods.toString = {
      value: Array.prototype.join
    };
    return Object.create(Array.prototype, methods);
  };
})();

We can now create “sub arrays” via makeSubArray function. It accepts one argument — an object with methods to add to [[Prototype]] of returned “sub array”.

var subMethods = {
  last: {
    value: function() {
      return this[this.length - 1];
    }
  }
};
var sub = makeSubArray(subMethods);
var sub2 = makeSubArray(subMethods);
// etc.

We can also hide this factory method behind a constructor, to make it similar to Array’s one:

var SubArray = (function() {
  var methods = { 
    last: { 
      value: function() {
        return this[this.length - 1];
      } 
    }
  };
  return function() {
    var arr = makeSubArray(methods);
    if (arguments.length === 1) {
      arr.length = arguments[0];
    }
    else {
      arr.push.apply(arr, arguments);
    }
    return arr;
  };
})();

And then use it as you would use regular Array constructor:

var sub = new SubArray(1, 2, 3);
 
sub.length; // 3
sub; // [1, 2, 3]
 
sub.length = 1;
sub; // [1]
 
sub[10] = 'x';
sub.push(1);

You can find this version of SubArray together with unit tests in Gtihub repository. For brevity, I made this implementation mainly take care of length/indices relation; certain methods (e.g. concat) do not behave identical to Array and need to be implemented accordingly.

[[Class]] limitations

The implementation we have just seen — the one utilizing property accessors — is great. It doesn’t require any host objects (such as iframes); it preserves relation between length and numeric properties; it even disallows out-of-range values for length or indices. All it requires is support for ES5 (or even just Object.create method).

But the dramatic title of this post is not there just for fun. There’s one little detail we’re missing in this otherwise complete implementation. And that detail is proper [[Class]] value — something that ECMAScript still doesn’t give full control over.

I wrote about [[Class]] before, when explaining how to detect arrays. In a nutshell, [[Class]] is an internal property of objects in ECMAScript. Its value is never exposed directly, but can still be inspected using certain methods (e.g. Object.prototype.toString). The usefulness of [[Class]] is that it allows to detect type of objects without relying on instanceof operator or checking object’s constructor — both of which fall short to detect objects from other contexts (e.g. iframes), as we’ve seen earlier.

Now, since objects created by makeSubArray are nothing but plain Object objects (only with special length getters/setters), their [[Class]] is also that of “Object” not an “Array”! We’ve taken care of length/indices relation, we’ve set up Array.prototype inheritance, but there’s no way to change object’s [[Class]] value. And so this solution can not claim to be complete.

Does [[Class]] matter?

You might be wondering — what are the actual implications of these pseudo-array objects having [[Class]] of “Object” not an “Array”. Do we even care? Well, for once, there’s an issue with object detection. Ironically, the solution I proposed to detect arrays relies on [[Class]], and so would fall short with objects like these.

// assuming that `sub` is a pseudo-array
Object.prototype.toString.call(sub) === '[object Array]'; // false

Another, probably more important, implication is that some of the methods in ECMAScript actually rely on [[Class]] value. For example, a well-known Function.prototype.apply accepts an array as its second argument (as well as an arguments object). Section 15.3.4.3 of ES3 says — “if argArray is neither an array nor an arguments object (see 10.1.8), a TypeError exception is thrown”. What this means is that if we pass pseudo-array object as a second argument to apply it will throw TypeError. apply doesn’t know or care if an object inherits from Array.prototype; neither does it care about object implementing special length/indices behavior. All it cares is that object is of proper type — type that we, unfortunately, can not emulate.

// assuming that `sub` is a pseudo-array
someFunction.apply(this, sub); // TypeError

There’s some vagueness in specification on this matter. For example, in Date.prototype.setTime spec says “If the this value is not a Date object, throw a TypeError exception.”, but in Date.prototype.getTime, it uses [[Class]] rather than just “not a Date object” — “If the this value is not an object whose [[Class]] property is “Date”, throw a TypeError exception”.

It’s probably safe to assume that these 2 phrases — “Date object” and “object with [[Class]] of ‘Date’” — have identical meaning. Ditto for “Array object” and “object with [[Class]] of ‘Array’”, as well as others.

Function.prototype.apply is not the only method sensitive to [[Class]] of an object. Array.prototype.concat, for example, follows different algorithm based on whether an object is an array or not (in other words — whether it has [[Class]] of “Array” or not).

// array ([[Class]] == "Array")
var arr = ['x', 'y'];
 
// object with numeric properties ([[Class]] == "Object")
var obj = { '0': 'x', '1': 'y' };
 
[1,2,3].concat(arr); // [1, 2, 3, 'x', 'y']
[1,2,3].concat(obj); // [1, 2, 3, { '0': 'x', '1': 'y' }]

As you can see, array values are “flattened”, whereas non-array ones are left as is. It is certainly possible to give these pseudo-arrays custom implementation of concat (and “fix” any other of Array.prototype.* methods), but the problem with Function.prototype.apply can not be solved.

It’s worth mentioning that another downside of accessor-based pseudo-array approach is performance. I haven’t done any tests, but it’s pretty clear that an implementation which has to enumerate over all numeric properties on every access of length property is not going to perform well. This is why I can’t recommend this solution for anything other than educational purposes.

Wrappers. Direct property injection.

Realizing a somewhat futile nature of subclassing arrays in Javascript often makes alternative solutions look very attractive. One of such solutions is using wrappers. Wrapper approach avoids setting up inheritance or emulating length/indices relation. Instead, a factory-like function can create a plain Array object, and then augment it directly with any custom methods. Since returned object is an Array one, it maintains proper length/indices relation, as well as [[Class]] of “Array”. It also inherits from Array.prototype, naturally.

function makeSubArray() {
  var arr = [ ];
  arr.push.apply(arr, arguments);
  arr.last = function() { 
    return this[this.length - 1];
  };
  return arr;
}
 
var sub = makeSubArray(1, 2, 3);
sub instanceof Array; // true
 
sub.length; // 3
sub.last(); // 3

While direct extension of array object is a beautiful, simplistic solution, it’s not without downsides. The main disadvantage is that on each invocation of constructor, an array needs to be extended with N number of methods. The time it takes to create an array is no longer a constant (if methods were on SubArray.prototype), but is directly proportional to the number of methods that need to be added.

Wrappers. Prototype chain injection.

To overcome the problem of “N methods”, another variation of wrappers can be used — the one in which object’s prototype chain is augmented, rather than object itself. Let’s see how this could be done:

function SubArray() { }
SubArray.prototype = new Array;
SubArray.prototype.last = function() {
  return this[this.length - 1];
};
 
function makeSubArray() {
  var arr = [ ];
  arr.push.apply(arr, arguments);
  arr.__proto__ = SubArray.prototype;
  return arr;
}

The idea is simple. When makeSubArray function is executed, two things happen: 1) an array object is created and is populated with any passed arguments; 2) object’s prototype chain is augmented in such way so that next object is SubArray.prototype, not original Array.prototype. The augmentation of prototype chain is done via non-standard __proto__ property.

But what happens in makeSubArray function is of course only half of the story. To make sure that object has Array.prototype in its prototype chain, we need to make SubArray.prototype inherit from it. This is exactly what’s being done on a second line of this snippet (SubArray.prototype = new Array). Prototype chain of an object returned from makeSubArray now looks like this:

new SubArray()
    |
    | [[Prototype]]
    |
    v
SubArray.prototype
    |
    | [[Prototype]]
    |
    v
Array.prototype
    |
    | [[Prototype]]
    |
    v
Object.prototype
    |
    | [[Prototype]]
    |
    v
   null

And because returned object is actually an Array, not an Object one, we also get length/indices relation as well as proper [[Class]] value. In fact, we can go even further and move initialization logic into SubArray constructor itself:

function SubArray() {
  var arr = [ ];
  arr.push.apply(arr, arguments);
  arr.__proto__ = SubArray.prototype;
  return arr;
}
SubArray.prototype = new Array;
SubArray.prototype.last = function() {
  return this[this.length - 1];
};
 
var sub = new SubArray(1, 2, 3);
 
sub instanceof SubArray; // true
sub instanceof Array; // true

Even though augmenting prototype chain is a more performant solution, there’s a clear downside — it relies on non-standard __proto__ property. ECMAScript, unfortunately, does not allow to set [[Prototype]] of an object — internal property referencing immediate ancestor in its prototype chain. Not even in 5th edition. Even though __proto__ is supported by a rather large number of implementations, it is far from being truly compatible.

Summary

So here it is; all the fun intricacies of subclassing arrays in Javascript.

We’ve seen that contrary to what might seem, actual inheritance is by far not the only aspect of subclassing arrays in Javascript; that arrays are different from regular objects by having special length/indices relation; how this length/indices relation is important and has nothing to do with prototype chain of an object; how arrays have special [[Class]] value of “Array” which is also rather important, and isn’t inherited either; how it’s not possible to change [[Class]] value of an object — not even in ECMAScript 5. We looked at different ways to “subclass” an array, starting from borrowing Array constructors from other contexts, and ending with augmentation of prototype chain. We examined benefits and downsides of each one of those solutions.

What we haven’t touched upon is the performance metrics of each of the implementations — perhaps a good topic for another discussion.

On this note, I leave you with a table summarizing pros/cons of the above mentioned techniques.

Proper [[Class]] length/indices Uses native objects only Requires ES3 only
Stack (Andrea Giammarchi) No No Yes Yes
IFrame borrowing (Dean Edwards) Yes Yes No Yes
Accessors No Yes Yes No
Direct extension Yes Yes Yes Yes
Prototype extension Yes Yes Yes No

[1] Whether this endeavor is something worth pursuing is a topic for another discussion

P.S. Big thanks to John David Dalton for reviewing an article and giving useful suggestions.

Archives Posts

JScript and DOM changes in IE9 preview 3

June 24th, 2010 by kangax

3rd preview of IE9 was released yesterday, with some amazing additions, like canvas element and an extensive ES5 support. I’ve been digging through it a little, to see what has changed and what hasn’t — mainly looking at JScript and DOM. I posted some of the findings on twitter, but want to also list them here, as it’s not very convenient to share code snippets in 140 characters. Referencing it all in one place will hopefully make it easier for IE team to find and fix these deficiencies.

ECMAScript 5 and JScript

The big news is that IE9pre3 has (almost) full support for ES5. By “full support”, I mean that it implements majority of new API, such as Object.create, Object.defineProperty, String.prototype.trim, Array.isArray, Date.now, and many other additions. As of now, IE9 implements the largest number of new methods; even more than latest Chrome, Safari and Firefox. Unbelievable, isn’t it? :)

screenshot of es5 compatibility table

You can see the results in this compatibility table (note that it lists results of mere “existence” testing, not any kind of conformance).

What’s missing is strict mode, which actually isn’t implemented in any of the browsers yet.

Some of the things I noticed:

ES5 Object.getPrototypeOf on host objects seems to lie, always returning null instead of proper value of [[Prototype]]:

  Object.getPrototypeOf(document.body); // null
  Object.getPrototypeOf(document); // null
  Object.getPrototypeOf(alert); // null
  Object.getPrototypeOf(document.childNodes); // null

This doesn’t happen in other browsers that implement Object.create at the moment, such as latest Chrome, WebKit or Firefox. In Chrome, for example:

  Object.getPrototypeOf(document.body) === HTMLBodyElement.prototype;
  Object.getPrototypeOf(document) === HTMLDocument.prototype;
  Object.getPrototypeOf(alert) === Function.prototype;
  Object.getPrototypeOf(document.childNodes) === NodeList.prototype

… and so on.

Interestingly, bound functions in IE9pre3 are represented as “function(){ [native code] }”, similar to host objects:

  var bound = (function f(x, y){ return this; }).bind({ x: 1 });
  bound + ''; // "function(){ [native code] }"
 
  // compare to
 
  alert + ''; // "function alert(){ [native code] }"

Note how function representation does not include identifier (f), parameters (x and y), nor representation of function body (return this;). This of course proves once again that relying on function decompilation is NOT a good idea.

Whitespace character class (as in /\s/) still doesn’t match majority of whitespace characters (as defined by specs). These include “U+00A0”, “U+2000” to “U+200A”, “U+3000”, etc. The test is available here. Curiously, ES5 String.prototype.trim seems to “understand” those characters as whitespace very well, producing empty string — as expected — for something like '\u00A0'.trim().

It was nice to see that ES5 Array.isArray is about 20 times faster than custom implementation, such as this one:

  function isArray(o) {
    return Object.prototype.toString.call(o) === "[object Array]";
  }

The difference in speed is similar to other browsers that implement this method.

An infamous, 10+ year-old JScript NFE bug, which I described at length before, is finally fixed:

  var f = function g() { return f === g; };
  typeof g; // "undefined"
 
  f(); // true

arguments’ [[Class]] is now an “Arguments”, just like ES5 specifies it:

  var args = (function(){ return arguments; })();
  Object.prototype.toString.call(args); // "[object Arguments]"

DOM

Unfortunately, the entire host objects infrastructure still looks very similar to the one from IE8. Host objects don’t inherit from Object.prototype, don’t report proper typeof, and don’t even have basic properties like “length” or “prototype”, which all function objects must have:

  alert instanceof Object; // false
  typeof alert; // "object"
  alert.length; // undefined

Because they don’t inherit from Object.prototype, we don’t have any of Object.prototype methods, naturally:

  alert.toString; // undefined
  alert.constructor; // undefined
  alert.hasOwnProperty; undefined

Object.prototype is not the only object host methods fail to inherit from. In majority of modern browsers, host objects also inherit from Function.prototype and so have Function.prototype methods like call and apply. This doesn’t happen in IE9pre3.

  alert instanceof Function; // false
  document.createElement instanceof Function; // false
 
  alert.call; // undefined

Curiously, call and apply are present on some host objects, but they are still not inherited from Function.prototype:

  typeof document.createElement.call; // "function"
  document.createElement.call === Function.prototype.call; // false

Host objects’ [[Class]] is far from ideal as well. IE9pre3 actually violates ES5, which says that objects implementing [[Call]] (or in other words — are callable) should have [[Class]] of “Function” — even if they are host objects. In IE9pre3, alert is a callable host object, yet it reports its [[Class]] as “Object” not “Function”. Not good.

  Object.prototype.toString.call(alert); // "[object Object]"
  Object.prototype.toString.call(document.createElement); // "[object Object]"

IE9pre3 still messes up DOM objects’ attributes and properties, although not as badly as earlier versions:

  var el = document.createElement('p');
  el.setAttribute('x', 'y');
  el.x; // 'y'
 
  el.foobarbaz = 'moo';
  el.hasAttribute('foobarbaz'); // true
  el.getAttribute('foobarbaz'); // 'moo'

Some old, humorous bugs can still be seen in IE9pre3, such as methods returning “string” when applied typeof on:

  typeof Option.create; // "string"
  typeof Image.create; // "string"
  typeof document.childNodes.item; // "string"

Undeclared assignments still throw error when same-id’ed elements are present in DOM, however not with same-name’ed elements (as it was in previous versions):

  <div id="foo"></div>
  <a name="bar"></a>
  ...
  <script>
    foo = function(){ /* ... */ }; // Error
    bar = function(){ /* ... */ }; // no Error
  </script>

Similarly to IE8, only Element and specific element type interfaces (HTMLDivElement, HTMLScriptElement, HTMLSpanElement, etc.) are exposed as same-named global properties. Node and HTMLElement are still missing, and element’s prototype chain most likely still looks like this:

  document.createElement('div');
    |
    | [[Prototype]]
    v
  HTMLDivElement.prototype
    |
    | [[Prototype]]
    v
  Element.prototype
    |
    | [[Prototype]]
    v
  null

…rather than what can be seen in almost all other modern browsers:

  document.createElement('div');
    |
    | [[Prototype]]
    v
  HTMLDivElement.prototype
    |
    | [[Prototype]]
    v
  HTMLElement.prototype
    |
    | [[Prototype]]
    v
  Element.prototype
    |
    | [[Prototype]]
    v
  Node.prototype
    |
    | [[Prototype]]
    v
  Object.prototype
    |
    | [[Prototype]]
    v
  null

getComputedStyle from DOM Level 2 is still missing, however its value is mysteriously a null, not undefined. The property actually exists on an object, but has a value of null. Hopefully, this is just a placeholder and proper method will be added before final release.

  document.defaultView.getComputedStyle; // null
  'getComputedStyle' in document.defaultView; // true

Array.prototype.slice can now convert certain host objects (e.g. NodeList’s) to arrays — something that majority of modern browsers have been able to do for quite a while:

  Array.prototype.slice.call(document.childNodes) instanceof Array; // true

That’s it for now.

Unfortunately, I don’t have much time to look into these things extensively, at the moment. There might be more updates on twitter.

As always, any corrections, suggestions, and additions are much appreciated.

Archives Posts

What’s wrong with extending the DOM

April 5th, 2010 by kangax

I was recently surprised to find out how little the topic of DOM extensions is covered on the web. What’s disturbing is that downsides of this seemingly useful practice don’t seem to be well known, except in certain secluded circles. The lack of information could well explain why there are scripts and libraries built today that still fall into this trap. I’d like to explain why extending DOM is generally a bad idea, by showing some of the problems associated with it. We’ll also look at possible alternatives to this harmful exercise.

But first of all, what exactly is DOM extension? And how does it all work?

How DOM extension works

DOM extension is simply the process of adding custom methods/properties to DOM objects. Custom properties are those that don’t exist in a particular implementation. And what are the DOM objects? These are host objects implementing Element, Event, Document, or any of dozens of other DOM interfaces. During extension, methods/properties can be added to objects directly, or to their prototypes (but only in environments that have proper support for it).

The most commonly extended objects are probably DOM elements (those that implement Element interface), popularized by Javascript libraries like Prototype and Mootools. Event objects (those that implement Event interface), and documents (Document interface) are often extended as well.

In environment that exposes prototype of Element objects, an example of DOM extension would look something like this:

  Element.prototype.hide = function() {
    this.style.display = 'none';
  };
  ...
  var element = document.createElement('p');
 
  element.style.display; // ''
  element.hide();
  element.style.display; // 'none'

As you can see, “hide” function is first assigned to a hide property of Element.prototype. It is then invoked directly on an element, and element’s “display” style is set to “none”.

The reason this “works” is because object referred to by Element.prototype is actually one of the objects in prototype chain of P element. When hide property is resolved on it, it’s searched throughout the prototype chain until found on this Element.prototype object.

In fact, if we were to examine prototype chain of P element in some of the modern browsers, it would usually look like this:

  // "^" denotes connection between objects in prototype chain
 
  document.createElement('p');
    ^
  HTMLParagraphElement.prototype
    ^
  HTMLElement.prototype
    ^
  Element.prototype
    ^
  Node.prototype
    ^
  Object.prototype
    ^
  null

Note how the nearest ancestor in the prototype chain of P element is object referred to by HTMLParagraphElement.prototype. This is an object specific to type of an element. For P element, it’s HTMLParagraphElement.prototype; for DIV element, it’s HTMLDivElement.prototype; for A element, it’s HTMLAnchorElement.prototype, and so on.

But why such strange names, you might ask?

These names actually correspond to interfaces defined in DOM Level 2 HTML Specification. That same specification also defines inheritance between those interfaces. It says, for example, that “… HTMLParagraphElement interface have all properties and functions of the HTMLElement interface …” (source) and that “… HTMLElement interface have all properties and functions of the Element interface …” (source), and so on.

Quite obviously, if we were to create a property on “prototype object” of paragraph element, that property would not be available on, say, anchor element:

  HTMLParagraphElement.prototype.hide = function() {
    this.style.display = 'none';
  };
  ...
  typeof document.createElement('a').hide; // "undefined"
  typeof document.createElement('p').hide; // "function"

This is because anchor element’s prototype chain never includes object refered to by HTMLParagraphElement.prototype, but instead includes that referred to by HTMLAnchorElement.prototype. To “fix” this, we can assign to property of object positioned further in the prototype chain, such as that referred to by HTMLElement.prototype, Element.prototype or Node.prototype.

Similarly, creating a property on Element.prototype would not make it available on all nodes, but only on nodes of element type. If we wanted to have property on all nodes (e.g. text nodes, comment nodes, etc.), we would need to assign to property of Node.prototype instead. And speaking of text and comment nodes, this is how interface inheritance usually looks for them:

  document.createTextNode('foo'); // < Text.prototype < CharacterData.prototype < Node.prototype
  document.createComment('bar'); // < Comment.prototype < CharacterData.prototype < Node.prototype

Now, it’s important to understand that exposure of these DOM object prototypes is not guaranteed. DOM Level 2 specification merely defines interfaces, and inheritance between those interfaces. It does not state that there should exist global Element property, referencing object that’s a prototype of all objects implementing Element interface. Neither does it state that there should exist global Node property, referencing object that’s a prototype of all objects implementing Node interface.

Internet Explorer 7 (and below) is an example of such environment; it does not expose global Node, Element, HTMLElement, HTMLParagraphElement, or other properties. Another such browser is Safari 2.x (and most likely Safari 1.x).

So what can we do in environments that don’t expose these global “prototype” objects? A workaround is to extend DOM objects directly:

  var element = document.createElement('p');
  ...
  element.hide = function() {
    this.style.display = 'none'; 
  };
  ...
  element.style.display; // ''
  element.hide();
  element.style.display; // 'none'

What went wrong?

Being able to extend DOM elements through prototype objects sounds amazing. We are taking advantage of Javascript prototypal nature, and scripting DOM becomes very object-oriented. In fact, DOM extension seemed so temptingly useful that few years ago, Prototype Javascript library made it an essential part of its architecture. But what hides behind seemingly innocuous practice is a huge load of trouble. As we’ll see in a moment, when it comes to cross-browser scripting, the downsides of this approach far outweigh any benefits. DOM extension is one of the biggest mistakes Prototype.js has ever done.

So what are these problems?

Lack of specification

As I have already mentioned, exposure of “prototype objects” is not part of any specification. DOM Level 2 merely defines interfaces and their inheritance relations. In order for implementation to conform to DOM Level 2 fully, there’s no need to expose those global Node, Element, HTMLElement, etc. objects. Neither is there a requirement to expose them in any other way. Given that there’s always a possibility to extend DOM objects manually, this doesn’t seem like a big issue. But the truth is that manual extension is a rather slow and inconvenient process (as we will see shortly). And the fact that fast, “prototype object” -based extension is merely somewhat of a de-facto standard among few browsers, makes this practice unreliable when it comes to future adoption or portability across non-convential platforms (e.g. mobile devices).

Host objects have no rules

Next problem with DOM extension is that DOM objects are host objects, and host objects are the worst bunch. By specification (ECMA-262 3rd. ed), host objects are allowed to do things, no other objects can even dream of. To quote relevant section [8.6.2]:

Host objects may implement these internal methods with any implementation-dependent behaviour, or it may be that a host object implements only some internal methods and not others.

The internal methods specification talks about are [[Get]], [[Put]], [[Delete]], etc. Note how it says that internal methods behavior is implementation-dependent. What this means is that it’s absolutely normal for host object to throw error on invocation of, say, [[Get]] method. And unfortunatey, this isn’t just a theory. In Internet Explorer, we can easily observe exactly this—an example of host object [[Get]] throwing error:

  document.createElement('p').offsetParent; // "Unspecified error."
  new ActiveXObject("MSXML2.XMLHTTP").send; // "Object doesn't support this property or method"

Extending DOM objects is kind of like walking in a minefield. By definition, you are working with something that’s allowed to behave in unpredictable and completely erratic way. And not only things can blow up; there’s also a possibility of silent failures, which is even worse scenario. An example of erratic behavior is applet, object and embed elements, which in certain cases throw errors on assignment of properties. Similar disaster happens with XML nodes:

  var xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
  xmlDoc.loadXML('<foo>bar</foo>');
  xmlDoc.firstChild.foo = 'bar'; // "Object doesn't support this property or method"

There are other cases of failures in IE, such as document.styleSheets[99999] throwing “Invalid procedure call or argument” or document.createElement('p').filters throwing “Member not found.” exceptions. But not only MSHTML DOM is the problem. Trying to overwrite “target” property of event object in Mozilla throws TypeError, complaining that property has only a getter (meaning that it’s readonly and can not be set). Doing same thing in WebKit, results in silent failure, where “target” continues to refer to original object after assignment.

When creating API for working with event objects, there’s now a need to consider all of these readonly properties, instead of focusing on concise and descriptive names.

A good rule of thumb is to avoid touching host objects as much as possible. Trying to base architecture on something that—by definition—can behave so sporadically is hardly a good idea.

Chance of collisions

API based on DOM element extensions is hard to scale. It’s hard to scale for developers of the library—when adding new or changing core API methods, and for library users—when adding domain-specific extensions. The root of the issue is a likely chance of collisions. DOM implementations in popular browsers usually all have properietary API’s. What’s worse is that these API’s are not static, but constantly change as new browser versions come out. Some parts get deprecated; others are added or modified. As a result, set of properties and methods present on DOM objects is somewhat of a moving target.

Given huge amount of environments in use today, it becomes impossible to tell if certain property is not already part of some DOM. And if it is, can it be overwritten? Or will it throw error when attempting to do so? Remember that it’s a host object! And if we can quietly overwrite it, how would it affect other parts of DOM? Would everything still work as expected? If everything is fine in one version of such browser, is there a guarantee that next version doesn’t introduce same-named property? The list of questions goes on.

Some examples of proprietary extensions that broke Prototype are wrap property on textareas in IE (colliding with Element#wrap method), and select method on form control elements in Opera (colliding with Element#select method). Even though both of these cases are documented, having to remember these little exceptions is annoying.

Proprietary extensions are not the only problem. HTML5 brings new methods and properties to the table. And most of the popular browsers have already started implementing them. At some point, WebForms defined replace property on input elements, which Opera decided to add to their browser. And once again, it broke Prototype, due to conflict with Element#replace method.

But wait, there’s more!

Due to long-standing DOM Level 0 tradition, there’s this “convenient” way to access form controls off of form elements, simply by their name. What this means is that instead of using standard elements collection, you can access form control like this:

  <form action="">
    <input name="foo">
  </form>
  ...
  <script type="text/javascript">
    document.forms[0].foo; // non-standard access
    // compare to
    document.forms[0].elements.foo; // standard access
  </script>

So, say you extend form elements with login method, which for example checks validation and submits login form. If you also happen to have form control with “login” name (which is pretty likely, if you ask me), what happens next is not pretty:

  <form action="">
    <input name="login">
    ...
  </form>
  ...
  <script type="text/javascript">
    HTMLFormElement.prototype.login = function(){ 
      return 'logging in'; 
    };
    ...
    $(myForm).login(); // boom!
    // $(myForm).login references input element, not `login` method
  </script>

Every named form control shadows properties inherited through prototype chain. The chance of collisions and unexpected errors on form elements is even higher.

Situation is somewhat similar with named form elements, where they can be accessed directly off document by their names:

  <form name="foo">
    ...
  </form>
  ...
  <script type="text/javascript">
    document.foo; // [object HTMLFormElement]
  </script>

When extending document objects, there’s now an additional risk of form names conflicting with extensions. And what if script is running in legacy applications with tons of rusty HTML, where changing/removing such names is not a trivial task?

Employing some kind of prefixing strategy can alleviate the problem. But will probably also bring extra noise.

Not modifying objects you don’t own is an ultimate recipe for avoiding collisions. Breaking this rule already got Prototype into trouble, when it overwrote document.getElementsByClassName with own, custom implementation. Following it also means playing nice with other scripts, running in the same environment—no matter if they modify DOM objects or not.

Performance overhead

As we’ve seen before, browsers that don’t support element extensions—like IE 6, 7, Safari 2.x, etc.—require manual object extension. The problem is that manual extension is slow, inconvenient and doesn’t scale. It’s slow because object needs to be extended with what’s often a large number of methods/properties. And ironically, these browsers are the slowest ones around. It’s inconvenient because object needs to be first extended in order to be operated on. So instead of document.createElement('p').hide(), you would need to do something like $(document.createElement('p')).hide(). This, by the way, is one of the most common stumbing blocks for beginners of Prototype. Finally, manual extension doesn’t scale well because adding API methods affects performance pretty much linearly. If there’s 100 methods on Element.prototype, there has to be 100 assignments made to an element in question; if there’s 200 methods, there has to be 200 assignments made to an element, and so on.

Another performance hit is with event objects. Prototype follows similar approach with events and extends them with a certain set of methods. Unfortunately, some events in browsers—mousemove, mouseover, mouseout, resize, to name few—can fire literally dozens of times per second. Extending each one of them is an incredibly expensive process. And what for? Just to invoke what could be a single method on event obejct?

Finally, once you start extending elements, library API most likely needs to return extended elements everywhere. As a result, querying methods like $$ could end up extending every single element in a query. It’s easy to imagine performance overead of such process, when we’re talking about hundreds or thousands of elements.

IE DOM is a mess

As shown in previous section, manual DOM extension is a mess. But manual DOM extension in IE is even worse, and here’s why.

We all know that in IE, circular references between host and native objects leak, and are best avoided. But adding methods to DOM elements is a first step towards creation of such circular references. And since older versions of IE don’t expose “object prototypes”, there’s not much to do but extend elements directly. Circular references and leaks are almost inevitable. And in fact, Prototype suffered from them for most of its lifetime.

Another problem is the way IE DOM maps properties and attributes to each other. The fact that attributes are in the same namespace as properties, increases chance of collisions and all kinds of unexpected inconsistencies. What happens if element has custom “show” attribute and is then extended by Prototype. You’ll be surprised, but show “attribute” would get overwritten by Prototype’s Element#show method. extendedElement.getAttribute('show') would return a reference to a function, not the value of “show” attribute. Similarly, extendedElement.hasAttribute('hide') would say “true”, even if there was never custom “hide” attribute on an element. Note that IE<8 lacks hasAttribute, but we could still see attribute/property conflict: typeof extendedElement.attributes['show'] != "undefined".

Finally, one of the lesser-known downsides is the fact that adding properties to DOM elements causes reflow in IE, so mere extension of element becomes a quite expensive operation. This actually makes sense, given the deficient mapping of attributes and properties in its DOM.

Bonus: browser bugs

If everything we’ve been over so far is not enough (in which case, you’re probably a masochist), here’s a couple more bugs to top it all of.

In some versions of Safari 3.x, there’s a bug where navigating to a previous page via back button wipes off all host object extensions. Unfortunately, the bug is undetectable, so to work around the issue, Prototype has to do something horrible. It sniffs browser for that version of WebKit, and explicitly disables bfcache by attaching “unload” event listener to window. Disabled bfcache means that browser has to re-fetch page when navigating via back/forward buttons, instead of restoring page from the cached state.

Another bug is with HTMLObjectElement.prototype and HTMLAppletElement.prototype in IE8, and the way object and applet elements don’t inherit from those prototype objects. You can assign to a property of HTMLObjectElement.prototype, but that property is never “resolved” on object element. Ditto for applets. As a result, those elements always have to be extended manually, which is another overhead.

IE8 also exposes only a subset of prototype objects, when compared to other popular implementations. For example, there’s HTMLParagraphElement.prototype (as well as other type-specific ones), and Element.prototype, but no HTMLElement (and so HTMLElement.prototype) or Node (and so Node.prototype). Element.prototype in IE8 also doesn’t inherit from Object.prototype. These are not bugs, per se, but is something to keep in mind nevertheless: there’s nothing good about trying to extend non-existent Node, for example.

Wrappers to the rescue

One of the most common alternatives to this whole mess of DOM extension is object wrappers. This is the approach jQuery has taken from the start, and few other libraries followed later on. The idea is simple. Instead of extending elements or events directly, create a wrapper around them, and delegate methods accordingly. No collisions, no need to deal with host objects madness, easier to manage leaks and operate in dysfunctional MSHTML DOM, better performance, saner maintenance and painless scaling.

And you still avoid procedural approach.

Prototype 2.0

The good news is that Prototype mistake is something that’s going away in the next major version of the library. As far as I’m concerned, all core developers understand the problems mentioned above, and that wrapper approach is the saner way to move forward. I’m not sure what the plans are in other DOM-extending libraries like Mootools. From what I can see they are already using wrappers with events, but still extend elements. I’m certinaly hoping they move away from this madness in a near future.

Controlled environments

So far we looked at DOM extension from the point of view of cross-browser scripting library. In that context, it’s clear how troublesome this idea really is. But what about controlled environments? When script is only run in one or two environments, such as those based on Gecko, WebKit or any other modern non-MSHTML DOM. Perhaps it’s an intranet application, that’s accessed through certain browsers. Or a desktop, WebKit-based app.

In that case, situtation is definitly better. Let’s look at the points listed above.

Lack of specification becomes somewhat irrelevant, as there’s no need to worry about compatibility with other platforms, or future editions. Most of the non-MSHTML DOM environments expose DOM object prototypes for quite a while, and are unlikely to drop it in a near future. There’s still a possibility for change, however.

Point about host objects unreliability also loses its weight, since host objects in Gecko or WebKit -based DOMs are much, much saner than those in MSHTML DOM. But they are still host objects, and so should be treated with care. Besides, there are readonly properties covered before, which could easily cripple the flexibility of API.

The point about collisions still holds weight. These environments support non-standard form controls access, have proprietary API, and are constantly implementing new HTML5 features. Modifying objects you don’t own is still a wicked idea and can lead to hard-to-find bugs and inconsistencies.

Performance overhead is practically non-existent, as these DOM support prototype-based DOM extension. Performance can actually be even better, comparing to, say, wrappers approach, as there’s no need to create any additional objects in order to invoke methods (or access properties) off DOM objects.

Extending DOM in controlled environment sure seems like a perfectly healthy thing to do. But even though the main problem is that with collisions, I would still advise to employ wrappers instead. It’s a safer way to move forward, and will save you from maintenance overhead in the future.

Afterword

Hopefuly, you can now clearly see all the truth behind what looks like an elegant approach. Next time you design a Javascript framework, just say no to DOM extensions. Say no, and save yourself from all the trouble of maintaining a cumbersome API and suffering unnecessary performance overheads. If on the other hand, you’re considering to employ Javascript library that extends DOM, stop for a second, and ask yourself if you’re willing to take a risk. Is ellusive convenience of DOM extension really worth all the trouble?

« Previous Entries