Tuesday, March 11, 2008

Sorry Dean, I subclassed Array again

Most library authors would love to extend the Array object (especially for those tasty iteration methods) but shy away from doing so for fear of breaking other scripts. So nearly all (with the noteable exception of Prototype) leave Array and other built-in objects alone.

This is how Dean Edwards started one of his (historic) post about subclassing Array, but I finally found the way to do it better, and You'll read why and how ... please be patience :D

The nightmare does not come only from IE


We all know how weird is Internet Explorer when you try to use an array as a constructor prototype.
What you probably do not know yet, is that IE is the only one that gives you problem "instantly instead of during". Let's start with the first, basic example:

// FireFox and Safari, plus IE, why not

function MyArray(){};
MyArray.prototype = [];

/** this should be a must ... however, who cares about that ...
MyArray.prototype.constructor = MyArray;
*/

var ma = new MyArray;
ma.push(1,2,3);
alert(ma); // 1,2,3 ... seems good, isn't it?

// now, lets use our Array thinking it's an array
var a = new Array(4,5,6);

// now, for some reson you should use your
// personal Array as is ... an Array, are you sure?
a.push.apply(a, ma);

/*
second arguments to Function.prototype.apply
must be an array

expected object or arguments

type error ...
*/

Well done ... we cannot use an instance that inherits from Array as an Array. The only one that seems to be clever enough to understand that ma is basically an Array, is Opera, well done Opera Team!.

The unespected instance


Another weird situation, using precedent constructor as example, is this one:

// Every browser you can try ...

function MyArray(){};
MyArray.prototype = [];

var ma = new MyArray;
alert(ma.concat(ma) instanceof MyArray); // false

false ??? ... What does it mean, false ?
False means that if we should be able to subclass an Array with IE too, there's no browser so clever to understand that internal methods should create an instance of the same constructor ... ok, ok, it's more close to ED209 in Delta City than reality ... anyway ...

I would like to obtain an instance of my constructor, and not an array.
This simply because we could extends whatever we want and after we use every native method, so fast and so powerful, we basically will lose our enhanced constructor coming back to a native Array.

In few words, if we should be able to extend Array, we should create wrappers for every method that returns, natively, an Array ... who talk about performances?
Just think that every this.slice() inside our prototypes should convert again the result if we would like to threat them as an instance of our super extended Array. ... does it sound still cool?
map, filter, sort, reverse ... and many others!

A clever solution, that noone seems to like so much


Basically, Dean found a solution that's clever and simple at the same time.
This one has not been used so much by libraries developers ... but why?
What I could suppose, is that there are a lot of limits:

  1. it depends on iframe creation, while JavaScript could be used everywhere, not only when an element is ready to recieve an iframe inside

  2. an iframe could means a lot of problems, specially in https sites, where some browser (of course IE) has a sort of paranoia if the iframe does not contain an src that points to a file in the same domain ... and sometime, even an empty html file to use as sentinel for empty iframes could be boring ...

  3. the iframe solution, has the same problem than Array extension ... if you want more power with native methods too, you have to wrap them!



Here is an example:

// directly from Dean Edward page
onload = function(){
var iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
frames[frames.length - 1].document.write(
"<script>parent.Array2 = Array;<\/script>"
);

// let's play
var a = new Array2(1,2,3);
alert(a); // 1,2,3 ... Yes!
alert(a.concat([4,5,6]) instanceof Array2); // FALSE AGAIN!!!
}


bloody hell, why the native concat method of my Array2 constructor returns an Array?

No way, if You use a native prototype, its behavior will be the expected one for current enviroment ... so as new Array ported inside the iframe will generate an instance of Array2 for every method that returns a partial or full copy of the Array.

More light in the black hole, please


Even if I agree with Dean when He says

shy away from doing so for fear of breaking other scripts. So nearly all (with the noteable exception of Prototype) leave Array and other built-in objects alone.

(removing the notable exception) ... I think that sometime prototypes could save our work, avoiding huge brainstormings to find alternative solution.

Array.prototype.to


Exactly, a stupid, short, simple, damned Array.prototype that has this goal:
Transform an Array like instance into a generic Array like instance (does it sound redundant?).

Array.prototype.to = function(constructor){
if(this instanceof constructor)
return this;
var self = new constructor;
Array.prototype.push.apply(self, Array.prototype.slice.call(this, 0));
return self;
};

This prototype could solve every kind of Array like transformation problems.

Do You need a jQuery from an Array ?

[document.body, document.getElementById("test")].to(jQuery).each(
function(element){
// do whatever stuff
});

You can use this stupid prototype to transform your results as well, but basically, you can use this prototype for every kind of Array like object.

jQuery.prototype.to = Array.prototype.to;
jQuery("input").to(Array).sort(function(el1, el2){
return el1.value < el2.value ? -1 : 1;
}).to(jQuery).doStuff();

Performances are really good, and compatibility is excellent. Starting from IE 5.5 and more, adding your own little simple push prototype plus a Function.prototype.call ... and you'll have support for IE4 too, even if this will not do make sense!

I know that a prototype to a global native variable is never a good thing to assing, but:

  1. Array or Array like objects (arguments) are often (always?) looped using their length and not using for in

  2. if you want to add aprototype, why do not add one that is strictly related with the same constructor and cosntructor like objects?



ArrayObject, and my last call for a truly subclassed Array


Thanks to precedent idea, the Array.prototype.to, I totally redraw my ArrayObject, now compatible with quite every JavaScript 1.6 method without other dependencies, and finally extendible in the most simple possible way, to create your own Array like library. Do you want an example?

// first of all, your constructor
function MyArrayLikeLib(){
// be sure that basic behavior is Array like
// using ArrayObject constructor
ArrayObject.apply(this, arguments);
};

// extend your constructor with an ArrayObject instance
MyArrayLikeLib.prototype = new ArrayObject;

// finally, add one or more prototypes to your personal
// constructor ... but DO NOT FORGET the constructor!
MyArrayLikeLib.prototype.constructor = MyArrayLikeLib;


var demo = new MyArrayLikeLib(1,2,3);
alert(demo); // 1,2,3
alert(demo.concat(new MyArrayLikeLib(4,5,6))); // 1,2,3,4,5,6
alert(demo.concat(new MyArrayLikeLib(4,5,6)) instanceof ArrayObject); // TRUE
alert(demo.concat(new MyArrayLikeLib(4,5,6)) instanceof MyArrayLikeLib); // TRUE


Thanks to the to prototype, natively created in the ArrayObject itself, you will recieve for every method that returns an array like object, an instance of your constructor.

How can it be possible?

// one ArrayObject prototype method, the slice one
slice: function(){
return Array.prototype.slice.apply(this, arguments).to(this.constructor);
}

It's diabolic simple if you remember to assign the real constructor to the prototype!

How to add prototype methods to ArrayObject inherited constructor


Uhm ... this is quite a newbie question, isn't it? :lol:
However, just forget the new Object assignment, or use the object in a different way:

function A(){ArrayObject.apply(this, arguments)};
A.prototype = new ArrayObject;
A.prototype.constructor = A;
(function(prototype){
for(var key in prototype)
A.prototype[key] = prototype[key];
})({
doMyStuff:function(stuff){
this.push(stuff);
return this.sort();
},
each:function(callback){
this.foreach(callback, this);
}
});


A simple benchmark


This page contains some simple speed challenge between native Array and ArrayObject.
Of course in some case ArrayObject is a bit slower, specially with Safari beta for widnows, but those are the worst case scenario, where for compatibility and unexpected behaviors reason I had to put more code, while every, forEach, indexOf, join, lastIndexOf, pop, push, reverse, shift, and finally some, are native when your browser is cool, are compatible and fast enough, when your browser is IE.

The bad news, what You cannot do with ArrayObject


You know, arguments is exactely an ArrayObject like variable ... and what's up if you do something like this?

function length(){
for(var i = 0; i < 3; i++)
arguments[arguments.length] = i;
alert(arguments[0]); // 2
alert(arguments[1]); // undefined
};

length();

You cannot use the length as a getted/setted property as is for native Arrays, but hey, you have native push method, why should you use the length in that way?
(speed? ... if you think that will boost up your applications, use native Arrays inside those loops, and finally convert them.to(ArrayObject)) ;)

Kind Regards


P.S. just another tricky usage of Array.prototype.to

function args2arr(){
arguments.to = [].to;
return arguments.to(Array);
};
alert(args2arr(1,2,3)); // 1,2,3

No comments:

Post a Comment