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.
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.
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.
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.
I found this article helpful in examining some of the hacks to determine whether or not an object is "most likely" an array.