Determining with absolute accuracy whether or not a JavaScript object is an array

Typing problems in JavaScript

JavaScript's typeof operator is well known to have confusing behavior: typeof null === "object", and typeof null !== "null". This mistake trips up newcomers, and every so often it'll trip up a seasoned yet forgetful veteran, but we've basically grown used to it. Perhaps more important, there's a failsafe workaround: simply compare directly as v === null to eliminate null from the possibilities under consideration.

Determining whether a value is an array

typeof null === "object" is perhaps the most common typing mistake in JavaScript, but there are others as well. A much less common but no less confusing problem is that of determining whether an object is an array. Surely, say you, this is a simple problem with a simple solution, like so:

if (o instanceof Array)
{
  // Oh frabjous day!
}

Under certain circumstances, the above code is perfectly functional; considering the history of the web, it's not surprising the issue wasn't quickly apparent. The problem arises when one considers an aspect of JavaScript in browsers not contemplated by ECMAScript: multiple globals.

The ECMAScript specification describes the environment and mechanisms involved in executing a string of code. The syntax and basic semantics of language constructs are certainly important, but without the built-in methods and objects coding in ECMAScript wouldn't be much fun. These methods and objects are accessed through the global object, and it is here where things go astray. The ECMAScript 3 environment implicitly assumes the existence of a single global (or, perhaps, of islands each of which is its own environment, with no interaction between them) and does not in any manner address the idea of multiple globals.

Multiple globals, however, are fundamental to the browser; each window object is the global object for the scripts its page contains or references. What about arrays in different windows? The shared-mutation hazard of having arrays in two coordinating windows be instances of the same Array constructor, sharing the same Array.prototype, is enormous when either page augments Array.prototype (not to mention the security problems when one page is malicious!), so Array and Array.prototype in each window must be different. Therefore, by the semantics of instanceof, o instanceof Array works correctly only if o is an array created by that page's original Array constructor (or, equivalently, by use of an array literal in that page). Pfui.

Are there any other methods of determining whether a value is an array that might work around this? o.constructor === Array is one, with the same problem as an instanceof check. Another option relies on so-called "duck typing", where if a value quackslooks like a duckan array then it is a duckan array. Along the constructor-checking lines, you could check for other array methods like push or concat, or perhaps for a length property, but these properties could exist in the same fashion on a non-array object. If you're willing to have false positives and negatives (assuming unconstrained input) that might be acceptable, but of course that won't always be the case. One test in this style is Object.prototype.toString.call(o) === "[object Array]", but that relies on Object.prototype.toString and Function.prototype.call not being changed (probably a good assumption but still fragile). It's also a bit more of an obvious hack than any of the other ideas.

Enter Array.isArray

For these reasons, ECMAScript 5 defines a method, Array.isArray, to completely address the problem. If the first argument provided is an array object created in any window at all, it returns true; if no arguments were provided or if the first argument wasn't an array object, it returns false.

function test(fun, expect) { if (fun() !== expect) alert("FAIL: " + fun); }
test(function() { return Array.isArray([]); }, true);
test(function() { return Array.isArray(new Array); }, true);
test(function() { return Array.isArray(); }, false);
test(function() { return Array.isArray({ constructor: Array }); }, false);
test(function() { return Array.isArray({ push: Array.prototype.push, concat: Array.prototype.concat }); }, false);
test(function() { return Array.isArray(17); }, false);
Object.prototype.toString = function() { return "[object Array]"; };
test(function() { return Array.isArray({}); }, false);
test(function() { return Array.isArray({ __proto__: Array.prototype }); }, false);
test(function() { return Array.isArray({ length: 0 }); }, false);

var w = window.open("about:blank");
w.onload = function()
{
  test(function() { return Array.isArray(arguments); }, false);
  test(function() { return Array.isArray(new w.Array); }, true);
};

Does this method provide any additional functionality beyond the hacks? One important use is for implementing variadic methods. Consider, for example, MochiKit's MochiKit.Base.flattenArray method, whose documentation states that it:

Return a new Array consisting of every item in lst with Array items expanded in-place recursively. This differs from flattenArguments in that it only takes one argument and it only flattens items that are instanceof Array.

If you happen to be writing JavaScript that crosses window boundaries passing around arrays, you're out of luck trying to use flattenArray. From the MochiKit interpreter, note the instanceof Array check, with a little reformatting:

>>> MochiKit.Base._flattenArray.toSource()
(function (res, lst) {
  for (var i = 0; i < lst.length; i++) {
    var o = lst[i];
    if (o instanceof Array) {
      arguments.callee(res, o);
    } else {
      res.push(o);
    }
  }
  return res;
})

This is currently cross-window FAIL. :-) MochiKit can't actually implement exactly what a strict reading of its documentation would claim. Using Array.isArray with a feature-detection guard, however, will make it work correctly on arrays not from the window in which MochiKit.Base is being used.

It's worth noting that MochiKit's not the only framework out there that tries to hackily determine whether a value is an array. Dojo, for example, has dojo.isArray with similar limitations, and I suspect this is a common problem across many JS codebases.

When can I use it?

Technically, the answer to this question is: as soon as you want! The usual feature-testing for Array.isArray allows you to use this method if it exists and ignore it if it doesn't. You will be able to meaningfully use it, however, starting with Firefox 3.6. WebKit nightlies have also added support for it, so it will presumably be in future Safari and Chrome releases. I don't know about other vendors, but Array.isArray's utter triviality to implement (it was a 15-line patch for Mozilla with no complexity whatsoever, and a good bit of that was SpiderMonkey coding idioms) suggests that if you ask, they will provide.

Acknowledgements

I found this article helpful in examining some of the hacks to determine whether or not an object is "most likely" an array.