My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Tuesday, May 20, 2008

Habemus Array ... unlocked length in IE8, subclassed Array for every browser

History


I do not know how many time, during these years, JavaScript Ninjas tried to subclass the native Array to create libraries over its powerful methods without losing performances. I have finally discovered the way to remove locked length from Internet Explorer 8, and to solve problems with every other browser.

We tried to inherit Array instead of Object


This is where my last trip started, simply looking at arguments behaviour. It was there, since 2000 when I started to code in JavaScript, and it was so simple that probably few developers thought about them!

var o = {
length:0,
push:Array.prototype.push,
toString:Array.prototype.join
};

o.push(1,2,3);
alert(o); // 1,2,3

arguments, in JavaScript, is an instanceof Object, and not an Array, as is in ActionScript since version 1.0
What we have done all this time, is to use Array.prototype methods injecting a basic object, with a simple length parameter, inside.
If an object with a length value can be used as an Array, why on heart above code should not work?

We all love prototypal inheritance, we all want an instanceof Array


If you try to inherit directly an array as prototype, Internet Explorer will fix every instance length property, destroying possibility to use simple for loop over generated values.

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

var a = new MyArray;
a.push(1,2,3);
alert(a.length); // 0 with every Internet Explorer

Problems are much more than a fixed length, as I wrote many months ago when I presented my ArrayObject.
On the other hand, this kind problem has been fixed for Internet Explorer 8, and 7 emulation.
Yes, finally I did it!

/**
* Choose a name for subclassed Array
*/
Stack = (function(){ // (C) Andrea Giammarchi - Mit Style License

/**
* Your personal Array constructor
*/
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);
};

// Solution 1:
// Declaration of generic function
// with an array as prototype
function Array(){};
Array.prototype = [];

// Solution 2:
// use the prototype chain to inherit
// Array constructor and its native prototype
Stack.prototype = new Array;

// Solution 3:
// overwrite inherited length with zero value
Stack.prototype.length = 0;

// Solution 4:
// redeclare toString method in this way
// to let JScript core feel better
Stack.prototype.toString = function(){
return this.slice(0).toString();
};

/**
* Return and assign subclassed Array
*/
Stack.prototype.constructor = Stack;
return Stack;

})();

Above code is the basis to create an alternative Array constructor that will be able to work as expected with every browser plus IE8, without length problems.
If you try to remove a single comma from some Solution, it will never work.
If you directly assign an array to the prototype, length will be fixed.
If you remove toString prototype, FireFox and others will not work as expected.

The definitive workaround for every browser


Since first part of this post could be used in every browser, starting from IE 5.5, these old browser can simply use a constructor with a prototype full of native methods, but without instanceof Array behavior.
At the same time, every other cool browser (Safari, Firefox, Opera) could use above code to have the same behavior of IE8.

This is the full cross browser Stack constructor

While this is an improvement over basic JS 1.5 Array, to have JS 1.7 methods too, natives with updated browsers, emulated in a fast standard way with every other.
Stack Extended JS 1.7 Subclassed Array

The last problem to solve, the concat method


concat, is as simple as truly bastard prototype!
There is no way to use native concat method, even with prototypal chain inherited native Array instances.
This is why I have normalized that method, in a Stack self compatible way.
On the other hand, you cannot send a Stack instance as concat parameter, but you can always use native slice method, fast as native one is.

Best performances ever


Yes, using native, incore, prototypes, makes your code execution faster.
This compatibility + benchmark page, can tell you more about this Stack implementation than me, specially with IE8, Safari, and Opera, where performances are neary the same of a generic Array.

FireFox is probably the one that has more problems to manage native code with dynamic constructors, but hey, I am talking about Firefox beta 3, while probably RC1 or nex release will be fast as Safari, or Opera, are.

What to do with Stack?


Libraries, libraries, and libraries, finally with core performaces, the possibility to truly extend the Array, removing every fake iframe, popup, whathever you have used during these days.

Have fun with Stack, and see you soon for some other cool example with them :geek:

22 comments:

tim said...

loosing != losing

Andrea Giammarchi said...

Cheers :D

tim said...

Coma != comma

This is great stuff. I'm going to think about using it.

Andrea Giammarchi said...

OMG, I wrote everything wrong ... cheers again ;)

Anonymous said...

Great work! :)
Whats this line do? I recognize the bitwise operators but am at a loss at what it could be doing.

= -1 < length && length === length << 1 >> 1 ? length : this.push(length);

Andrea Giammarchi said...

I did some test and it seems that double bitwise operation is the fastest way to trasform something into an integer.

That line, basically, does the same thing of this one:
if(length === parseInt(length))
but it does not call a function for each created Stack, so it is truly fast

Anonymous said...

Hi, I'm afraid to be a little bit off topic but,

Your code use the "something = (//some code here)();" notation which I truly don't understand...

Would you give some resource links about it ?

Thanks

Alex (javascript rookie)

Andrea Giammarchi said...

JavaScript Closures :)

Anonymous said...

Hu,

I assumed this was related to closures but I still didn't understand the semantic of "()();". According to your answer it must be obvious (that make me feel more ashamed than ever)...

Reformulating it I was about to ask again: what's the difference between
foo = function(){//some code};
and
foo = (function(){//some code})();
?

Actually using a simple alert("hello dummy") code made it obvious ! !
the "(//a function)();" make the function in first parenthesis to be executed. Delirious.

Is there anything else I should understand about it ?

Thanks

Alex

Andrea Giammarchi said...

Try this:

(function(){})(); // OK
function(){}(); // syntax error

Since without brackets a function could cause a syntax error ( the exact case is when you are not assigning a value ), and since I am a lazy programmer :), I automatically use to wrap the function with brackets, every time I think to write a function.

This allows me to create and debug without problems, if I am assigning that function to something or not, during development (test in scope, for example ...).

As sum, this is OK:
var a = function(){}();

But I think brackets do not cause any problem to anyone, but if you think it is a shame, just remove those brackets. I will probably do it next Stack update (I forgot them, but it is not a problem) ... hoping this comment will answer you, regards.

Andrea Giammarchi said...

... ehr ... I have just realized tat you were talking about the inline function execution ... sorry for above boring comment.

Anyway, I hope that comment will give you another info, when you will start to use closures with inline function and return value assignment ;)

Anonymous said...

why:

-1 < length && length === length << 1 >> 1 ? length : this.push(length)

instead of:

typeof length == "number" && length > -1 ? length : this.push(length)

Isn't second one cleaner, shorter and probably faster ?

It took me a while to get what author had on his mind :)

Andrea Giammarchi said...

Medyk ... try with 1.23 ... a length has to be an unsigned integer, as specs says :)

Anonymous said...

ah.. I see now :)

Unknown said...

If you have a moment to spare, could you share with my limited little mind some usages for this, and what did you have in mind when you say libraries, libraries...

Andrea Giammarchi said...

Problem 1: everyone would like to extend the Array, causing conflicts between libraries - fixed

Problem 2: every library has methods like add, reverse, each, and those method use a wrapper function to native Array prototype methods - fixed

Finally, core functions are 10 times faster than dynamics, you can use push, shift, map, filter, forEach, without losing performances.

If we all tried to solve this problem, I guess there is a valid reason, and I hope you can find a valid example in the other post, about extending Stack to create an advanced subclassed constructor

Damian Wielgosik said...

Hi Andrea,

One point - what does happen when you overwrite the native Array constructor?

Regards and congrats

Andrea Giammarchi said...

do you mean add prototypes or literally overwrite the constructor?

In the first case you can have problems with idiots that use "for in" loops over arrays, in the second one you are missing literals for arrays definition.

function Array(){};
alert([] instanceof Array);

Above snippet will produce false, will break every "new Array" after its declaration plus won't bring anything to those variables already defined as new Array.

In 4 words: do NOT do it!

Regards

deadlyicon said...

Have you tried this approach? It seems to work for me in all browsers

http://gist.github.com/296559

also do you have any tests? I'd love to run them against this solution.

thanks!

Andrea Giammarchi said...

you did not test IE and the length property, did you?

Zaggi said...

Hi, what about this?

var normalArray = [];
normalArray.push("a");
normalArray.push("b");
normalArray.push("c");
normalArray.splice(1,1);

var stack = new Stack();
stack.push("a");
stack.push("b");
stack.push("c");
stack.splice(1,1);

alert(normalArray); // ok - a,c
alert(stack); // ok - a,c

alert(normalArray[2]); // ok - undefined
alert(stack[2]); // BUGGY - c

normalArray.length++;
stack.length++;

alert(normalArray); // ok - a,c,
alert(stack); // BUGGY - a,c,c

Andrea Giammarchi said...

IE does not support getter/setter so far, at least not for common prototypes so the length++ "BUGGY" is completely expected. I did not get the other error tho