Useful Platitudes

Notes on programming by Daniel Mendel

Subclassing Javascript Arrays

When I started working on the animation framework glitz.js, I wanted an easy syntax for expressing and traversing a nested tree of Renderable objects. I thought, ”Why not implement it as an array?” Since the scene graph would need to be iterated over very quickly during the render loop, it would be great to just represent the whole thing as a multi-dimentional array – essentially a subclass of array that had some additional properties and methods like render and animate.

1
2
3
4
5
var box1 = new Box({ x: 100, y: 100 });
var box2 = new Box({ x: 110, y: 110 });
box1.push( box2 );
box1[0];       // box2
box1.render(); // draws a box

1. Ah, Naïveté

First I tried a simple approach.

1
2
3
4
5
6
7
var arrLike = { length: 0 };
arrLike.push = function(){
  Array.prototype.push.apply( this, arguments );
}
arrLike.push('foo');
arrLike[0]   // 'foo'
arrLike.length // 1

Basically, you can invoke methods from the native Array.prototype on any object with a length and get array-like results. This works because javascript objects allow numerical keys, so essentially what we end up with is:

1
2
3
4
var arrLike = {
    0: 'foo'
  , length: 1
};

At first glance (eg, not in IE<8), this seems to work great. However, there are hidden dragons – let’s compare some further manipulations to a native array.

1
2
3
4
5
6
7
8
9
10
11
12
var arr = new Array('foo');
arr[0];         // 'foo'

arr.length     = 0;
arrLike.length = 0;
arr[0];         // undefined
arrLike[0]      // 'foo'

arr[10]     = 'bar';
arrLike[10] = 'bar';
arr.length;     // 11
arrLike.length; // 0

Uh-oh, there are obviously some drawbacks here. First, setting the length property of a proper Array to 0 clears the array. In fact, this is usually the fastest method of clearing arrays. Second, when you assign a value to an array index that is > length, then the native array will expand to contain it. In short, native arrays have a magical length that we miss out on entirely with the naive approach. As usual, more information can be found in the relevant section of the EMCAscript spec.

2. Direct extension

Okay, so my naive approach is dead in the water – after grieving, I decided to try direct extension. This is exactly what it sounds like: creating Array instances and copying a bunch of methods & properties on to them directly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var extension = {
    x: 0
  , y: 0
  , render: function(){ ... }
  // ...
};

var Box = function( instanceProps ){
  var arr = [], k;
  for( k in extension ){
      if( extension.hasOwnProperty(k) )
          arr[k] = extension[k];
  }
  for( k in instanceProps ){
      if( instanceProps.hasOwnProperty(k) )
          arr[k] = instanceProps[k];
  }
  return arr;
};

The outcome here is exactly what we want – a native array with some extra methods and properties. There’s just one problem, compared to creating raw instances of a native array, it is really, really slow – for example, in Firefox 17.0, it is 26 x slower when adding just two properties.

Now obviously there are a lot of use cases where this performance hit isn’t particularly painful – even at 1/26th speed you can still create many thousands of arrays per second, and probably most uses for glitz.js wouldn’t suffer too badly here. But, this is in an animation framework and if we can speed any part of it up by that much, it increases the domain it can operate in by a fair margin. This brings us to…

3. Having your cake and eating it inside of an iframe

Wouldn’t it be the best if we could just subclass Array and add some methods to the prototype? Of course this would contravine several of the golden rules of javascript best practices, first and foremost being don’t modify objects you don’t own. Another issue with this is that each user-defined Renderable subclass requires a different set of extentions to the prototype and trying to use prototypical inheritance runs into the same issues as the naive solution.

Enter Dean Edwards iframe sandbox solution – the idea here is that you create a hidden <iframe> and steal the Array object from the iframe execution context.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var iframe = document.createElement("iframe");
    iframe.style.display = "none";
    document.body.appendChild(iframe);
    frames[frames.length - 1].document.write(
      "<script>parent.myArray = Array;</script>"
    );

myArray === Array // false
myArray.prototype.foo = 'bar';

var arr   = new Array(1,2,3);
var myarr = new myArray(1,2,3);

myarr.foo;        // 'bar'
arr.foo;          // undefined

This works because browsers sandbox the javascript execution environments so that each frame owns a unique set of native objects. Here we simply “borrow” ( read: steal ) one from a new iframe and send it back to our execution environment. As you might imagine, this technique comes with some caveats:

  • Keep that iframe around and attached to the DOM – it still technically owns our array.
  • This approach doesn’t work in non-browser environments.

Number 2 is not a problem this purpose as glitz.js is already tied to the browser in other ways. Additionally, if it were ever to be ported to a server context this same methodology could be recreated with a slightly different technique, such as borrowing Array from a VM in node.js.

Ultimately this is the technique I used for a major update to the way that glitz.js handles array subclassing under the hood in commit 5c5d183 and also ended up putting together gimme a tiny stand alone library to automate the process of “borrowing” natives.

For the most comprehensive overview of the techniques for subclassing arrays in javascript, read How ECMAscript 5 still does not allow to subclass an array [sic].