Friday, April 03, 2009

A fast Array slice for every browser

It is an extremely common task and one of the most used prototype in libraries and applications: Array.prototype.slice
The peculiarity of this prototype is to create an Array from an Array like Object such arguments, HTMLCollection, other kind of lists as jQuery results.

Every browser, except Internet Explorer, allows direct calls via native prototype obtaining, obviously, best performances.

In IE, we have different ways to make this task possible, and these ways are mainly classic loops, or the isArray check to know when it is possible to perform the native call or not.


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

Even using a closure, above function could slow down performances, specially in those libraries where Array conversions are performed almost everywhere.

As we all know, Internet Explorer behaves weird "sometimes", and one of the weirdest things is that native Objects are not instanceof Object.

In few words, instead of call a function to define if native slice could be applied, all we need to do is to check if the generic object is instanceof Object.
In this way native Arrays, arguments, everything with a length user defined, will be passed via native Array.prototype.slice, while for native objects, the good old loop will do the dirty job.

A better general purpose slice

$slice = (function(slice){
// WebReflection - Mit Style License
try {
// Chrome, FireFox, Opera, Safari, WebKit
slice.call(document.childNodes);
var $slice = slice;
} catch(e) {
// Internet Epxlorer
var $slice = function(begin, end){
// false with native objects/collections
// suitable for Array like Object (e.g arguments, jQuery, etc ...)
if(this instanceof Object)
return slice.call(this, begin || 0, end || this.length);
// ... every other case ...
for(var i = begin || 0, length = end || this.length, len = 0, result = []; i < length; ++i)
result[len++] = this[i];
return result;
};
};
return $slice;
})(Array.prototype.slice);

Simple, isn't it? And here a test case:
(function(){
try {

// HTMLCollection [object, ...]
$slice.call(document.getElementsByTagName("*"));

// arguments [3,2,1]
$slice.call(arguments);

// Array [1,2,3]
$slice.call([1,2,3]);

} catch(e) {

// should never happen
alert(e.message);
};
})(3,2,1);

where if nothing happen, simply means $slice worked without problems.

Side effects? With sandbox variables (iframes) IE will always use the loop, unless we do not bring its native Object constructor into the function (or the function into the sandbox)

3 comments:

a.in.the.k said...

IMHO:the copy loop is broken, does not work for negative begin and end params

the same code is in your ArrayObject as well, so please fix both
TC:

6,0 //zero is bug 5, 6-1 should be result
3,2

var ao = document.getElementsByTagName("*");
alert($slice.call(ao).length);
alert($slice.call(ao, 0, -1).length);

var a = [1, 2, 3];
alert($slice.call(a).length);
alert($slice.call(a, 0, -1).length);

a.in.the.k said...

No reply ?

Andrea Giammarchi said...

holidays man, I'll check ASAP and thanks for the hint ;)