Monday, January 12th, 2009

isArray: Why is it so bloody hard to get right?

Category: JavaScript

pre>

A newcomer to JavaScript would assume that there is a simple way to see if an object is in fact an array. For the veterans out there, they know that demons are out there, especially when you are doing work that access cross frame code. Java folks have had this with class loader issues, where you get objects from different class loaders and weird things happen.

Kangax has detailed the issue and even offers the latest and greatest in the art, which has lead to Prototype and other libraries changing their implementation from duck typing to:

javascript
< view plain text >
  1. function isArray(o) {
  2.   return Object.prototype.toString.call(o) === '[object Array]';
  3. }

Related Content:

Posted by Dion Almaer at 5:12 am
27 Comments

+++--
3.6 rating from 27 votes

27 Comments »

Comments feed TrackBack URI

Douglas Crockford called this “The Miller Device”:

http://blog.360.yahoo.com/blog-TBPekxc1dLNy5DOloPfzVvFIVOWMB0li?p=916

Comment by backa — January 12, 2009

var is = {
types : ["Array","RegExp","Date","Number","String","Object","HTMLDocument"]
};

for(var i=0,c;c=is.types[i++];)
{
is = (function(type)
{
return function(obj)
{
return Object.prototype.toString.call(obj) == "[object "+type+"]";
}
})(c);
}

Comment by BenGerrissen — January 12, 2009

Whoops, the above code allows for adding more object types by:
is.types.push(“HTMLDivElement”)
.
And you can use it like so:
is.Array([])
.
or:
is.Date(new Date())

Comment by BenGerrissen — January 12, 2009

As response to the post by kagax I’ve written a blog post about type checks for native JavaScript types.

@Ben Note that class names for non native JavaScript types are implementation dependent. Internet Explorer for example does not use the class ‘HTMLDivElement’. IE always uses ‘Object’ for DOM elements.

Comment by fjakobs — January 12, 2009

@fjakobs
Yeah, you’re right, however it’s a nice way to test if a browser supports Element object types, which in the case of prototype.js might make browser sniffing more redundant, so I included it anyways to make people think ;)

Comment by BenGerrissen — January 12, 2009

erm.. is.types.push(”HTMLDivElement”) obviously doesn’t add a new type check function… 1 min coding ftw.

Comment by BenGerrissen — January 12, 2009

It does not work with sub classed Arrays, and this is truly annoying.

new Stack instanceof Array === true
{}.toString.call(new Stack) === “[object Object]“

Comment by WebReflection — January 12, 2009

@WebReflection

I assume that’s because “Stack” is indeed an Object object. I’m not sure what kind of implementation you’re talking about, but [[Class]] value is not inherited via [[Prototype]], afaik.

Comment by kangax — January 12, 2009

@BenGerrissen

Just because `[[Class]]` of certain host objects is represented as “HTMLDivElement” in a browser doesn’t really mean that that browser supports, say, `Element.prototype` inheritance, and/or have `.constructor` referencing globally declared `HTMLDivElement` constructor, etc. : )

Specification happens to be silent in this regard.

Comment by kangax — January 12, 2009

@kangax, more than inherited I hoped it was searched into the proto. the Stack constructor is a prototype = [] and each instance an instanceof Array. Btw, I was not expecting an [object Array] on a primitive call with Object.prototype.toString and my comment was only to underline that is not 100% reliable and all this stuff means that the “war about our own Array prototypes” won’t stop until JS2
I am still up for JSL ( JavaScript Standard Library … that I could probably update removing IE 5 and 5.5 fixes ) to normalize up to JS 1.6 or greater the Array prototype ( forEach instead of every different each, for example ) but I am sure somebody will find a perfect way to recognize an array before every library will implement standard behaviours ( or, even better, why don’t we agree about a single un-official method to retreive an array, like a toArray prototype that will return this if it is an array, slice.call(this, 0) in every other case )

Comment by WebReflection — January 12, 2009

As WebReflection pointed out, this technique has it’s own issues. If you are in a single frame or not sharing arrays between frames, it is actually really better and safer to use instanceof Array (where there is only one Array constructor) and avoid a library isArray or this toString based check. Since it is difficult for a framework to know what the context is, the library abstraction can be counter-productive except to help with cross-frame array sharing.

Comment by kriszyp — January 12, 2009

P.S. … what about this? … lol!

function isArray(Object){
var result = false;
try{new Object.constructor(Math.pow(2, 32))}catch(e){result=/Array/.test(e.message)};
return result;
};

alert(isArray([]));

Comment by WebReflection — January 12, 2009

@kriszyp
Yes, indeed; I don’t see a reason not to use `instanceof` in “single-frame” applications.

The main downside I see in retrieving [[Class]] value is that we don’t know how host objects implement it. They could be designed to throw error when [[Class]] lookup occurs (just like IE is known to throw for internal [[Get]] of certain objects – notably AcitveX components), or even collide with spec-defined values for native objects – “Array”, “Function”, etc. We can either do extensive testing or just deal with the fact that host objects’ behavior can never be relied upon.

Comment by kangax — January 12, 2009

@WebReflection

There’s an easier way, really : )

function isArray(o) {
try {
Array.prototype.toString.call(o);
return true;
} catch(e) { }
return false;
};
isArray([]); //true
isArray(); // false

See http://github.com/kangax/protolicious/tree/master/object.extensions.js#L1 for explanation

Comment by kangax — January 12, 2009

nice one kangax, I was joking but your one seems to make sense :D

(end even if I prefere this one, I am sure performances are not the same of the other toString trick)

Comment by WebReflection — January 12, 2009

I bet sooner or later someone’s going to come along and say that this technique is not secure because “Object.prototype.toString” can be overridden.

Comment by Jordan1 — January 13, 2009

Well.. if anyone extends Object.prototype, this technique not working is the last of his or her problems. Microwave Ovens will kill your beloved pets if you use them to dry their fur, so you can call those insecure, though if you read the manual, you would’ve known it would kill your cat.

Comment by BenGerrissen — January 13, 2009

if you define const NativeString = Object.prototype.toString; as first line of your application at least some recent browser will be secure enough ;-)

Comment by WebReflection — January 13, 2009

Brilliant as an idea. The only problem that I see is that it’s quite slower:

>>> test = []
[]
>>> debug.benchmark(util.isArray,[test],null,200000)
The function took 0.00104ms to execute and returned true
>>> isArr2 = function(val) { return Object.prototype.toString.call(val) == '[object Array]'; }
function()
>>> debug.benchmark(isArr2,[test],null,200000)
The function took 0.002125ms to execute and returned true

Ok not slow as a function in general, but 20 times slower compared to the instanceof way.

Perhaps a solution would be to check the length of window.frames and choose the method according to that, but then we’d still face issues with:
1. Scripted new windows.
2. Sublclassed arrays. I find it ok that the new method returns false for them, but it should be consistent. It should return either always true, or always false for subclassed arrays. It can’t return true for them and if you add a frame suddenly false for them, or true if the suclassed array belongs to the current window and false if it belongs to a frame!

Combining both methods with the || operator also suffers from problem 2, and would be even slower when the object is NOT an array.

A method like isArray should be lightning fast since it’s used very often. So even though I admire the brilliance of this solution, I’m still uncertain whether to use it.

Comment by LeaVerou — January 13, 2009

@Jordan1

AFAIK, `Object.prototype.toString` is generic. In case of “emergency”, it is possible to “restore” it from the “clean” context of newly created frame (in clients which support frame creation/inspection):


Object.prototype.toString = (function(){
var root = (document.body || document.documentElement);
var el = document.createElement('iframe');
root.appendChild(el);
var s = window.frames[window.frames.length-1].Object.prototype.toString;
root.removeChild(el);
return s;
})();

Comment by kangax — January 13, 2009

@LeaVerou

Interesting. My results are actually less dramatic (FF3.0.5 on Mac OSX) and seem to be negligible even under `50000` iterations. The difference is still there, of course.


var arr = [1, 'a', /foo/, NaN];
var LIM = 50000;

console.time('instanceof');
for (var i=0; i<LIM; i++) { arr instanceof Array; }
console.timeEnd('instanceof');

console.time('constructor');
for (var i=0; i<LIM; i++) { arr.constructor == Array; }
console.timeEnd('constructor');

console.time('[[Class]]');
for (var i=0; i<LIM; i++) { Object.prototype.toString.call(arr) == '[object Array]' }
console.timeEnd('[[Class]]');

instanceof: 86ms
constructor: 108ms
[[Class]]: 131ms

Regarding “sub-arrays”, it should be trivial to special-case `isArray` to account for them. E.g.:


function Sub(){};
Sub.prototype = new Array();
var sub = new Sub();
sub instanceof Array; // true

function isArray(o) {
return Object.prototype.toString.call(
// to prevent constructor-less host objects from blowing up
o.constructor ? o.constructor.prototype : o);
};

isArray(sub);

You could of course change it to account for arbitrary-level “sub-arrays” (i.e. check up the prototype chain till reaching `null`)
I agree that this “workaround” is not “ideal”. Unfortunately, so is our current state of JS/DOM affairs : )

Comment by kangax — January 13, 2009

How about this??

Array.prototype.__isArray__ = true;
function isArray(o) {
return (o && o.__isArray__) ? !!o.__isArray__ : false;
};

alert(isArray([])+ ‘,’ + isArray({})+ ‘,’ + isArray(null));
// true,false,false

Comment by mjuhl — January 15, 2009

@kangax: I was more concerned about the speed, rather than the subclassed arrays issue (which I consider quite trivial). Tests like Slickspeed aren’t very forgiving, and punish you for even such small differences.
After doing more tests like the above, I noticed that the difference in speed is not always as dramatic, but the best result I got was still 2 times slower than the instanceof method. I’m still undecided between “fast and correct most of the time” and “slower but always correct”. I guess I’ll end up with the toString() method after all, but I can’t decide now.

@mjuhl: This suffers from the same issues as the instanceof method and additionally its more obtrusive, since it adds an extra useless property to the Array prototype. Also, the ternary operator is redundant (return !!(o && o.isArray); would suffice), and so is the conversion to boolean (since __isArray__ is already boolean).

Comment by LeaVerou — January 19, 2009

here is my idea for an isArray function that is relatively speedy and detects Arrays, Subclassed Arrays and Arrays in separate frames.

@kangax: There is a problem with using Array.prototype.toString.call(obj) with arrays with several items i.e. 1000 with firefox 3.x running it over 50000 iterations is unacceptably slow i.e. 15-20 secs.

The idea is sound but toString is the wrong function to use. I did some experimenting and I came up with this function drawing from several other peoples Ideas.

function isArray(obj) {
if (obj instanceof Array) {
return true;
} else {
try {
if (obj.length > 0 && Array.prototype.slice.call(obj,0,1)[0] === obj[0]) {
return true;
} else {
if (Array.prototype.slice.call(obj,0,1).length === obj.length) {
return true;
}
}
}catch (e) {}
return false;
}
}

This function is lightning fast for regular arrays or sub classed arrays on pages without frames as it is just a obj instance of Array call. For pages with frames it is a single call to Array.prototype.splice.call(obj,0,1) which is quick regardless of array length if the array has elements it makes sure the element it received is equal to the first item in the array, if it is an empty array it compares the length of Array.prototype.splice.call(obj,0,1) with the original obj.length both of which should be zero.

for 50000 iterations the timing was
instanceof: 19ms
isarrayRegularArray: 48ms
isarrayFrameArray: 245ms

So for objects that are instances of Array according to Javascript the function call is approximately 2.5 times slower than the builtin instanceof command. for arrays in separate frames it is approximately 12-13 times slower. most importantly Array size is also not a contributing factor ran the tests with a 1000 element array and a 10000 element array with no time difference.

Comment by JasonP — January 19, 2009

@LeaVerou: Thanks, I was really just being facetious though. On a more serious note, why not do it this way:

function isArray(object) {
if (object instanceof Array) return true;
return Object.prototype.toString.call(object) === ‘[object Array]‘;
}

That way you’re covering your bases pretty well.

Comment by mjuhl — January 28, 2009

This is the simplest and easiest way I can think of:


function isArray(array){
try{ return !!array.push; }catch(e){ return false; }
}

I’ve tested this and it works fine as far as I see.

Comment by sekostar — June 9, 2009

Sorry this is even simpler


function isArray(array){
return array? !!array.push : false;
}

Comment by sekostar — June 9, 2009

Leave a comment

You must be logged in to post a comment.