Update, 10th July 2011 — It appears IE9 regressed this bug, so I updated the conditional compilation fix I wrote to work for all IE versions. I prefer to use feature detection where available, but I don’t see this being feasible. The bug is still present as of IE 10 PP2, but the code could easily be adjusted to check for IE less than 10 if the problem is fixed again before the final release.
I’ve been working on something lately that needs to check if a variable is an array. It’s a well known fact among JavaScript developers that, unfortunately, this is one of those situations where the typeof operator just doesn’t cut it. For a long time, many developers and libraries – jQuery, for one, used the following method to test if a variable is an array:
function isArray(v) {
return v instanceof Array;
}
This checks to see if the passed in variable inherits from the native Array constructor function. The problem with this is that when we reference Array, we know that it’s a property of the current global object – in the case of a browser, it’s a member of window. Each browser window is a separate context with its own set of properties and methods, so the Array constructor isn’t the same as that of another construct. What this means for the above piece of code is, if the function is passed an array from a different context, that array isn’t an instance of the current context’s Array constructor.
Early last year, kangax (I mentioned him in the Object.keys post) came to the rescue with a robust solution that worked across frames:
function isArray(v) {
return Object.prototype.toString.call(v) == "[object Array]";
}
This is knowledge of the specification for you. Array.prototype.toString loops through the array items and calls their toString method, returning the result as a combined string joined with a comma. Object.prototype.toString, on the other hand, will always return a string in the form of "[object [[Class]]]", where [[Class]] is the name of the internal class of the object. Because the internal class of an array is Array, no matter what context it comes from, we can be sure that this code will always work. Popular libraries like Prototype and jQuery are using this method right now. For those of you thinking, “all’s well that ends well”, I’m going to point you in the direction of the master of raining on parades; Internet Explorer.
Of course, this isn’t the case in Internet Explorer, which behaves rather strangely when passing variables between contexts. When running anything passed between contexts through Object.prototype.toString, the result becomes "[object Object]", including primitives. I’m going to go out on a limb here and speculate that this is due to JScript being heavily COM based – variables must become “host objects” when being passed (via COM) to another context.
In Internet Explorer 9, ECMA-262 5th edition comes to our rescue once again with the long overdue Array.isArray() function. Pass in a variable and it will spit out a boolean, true for arrays and false for anything else. Most other browsers also have this function at the time of writing. But we’re still left with a problem in IEs less than 9.
Potential Solution the first
I played with a lot of solutions, mostly involving duck-typing (which kangax goes into in more detail in his post), but nothing comes close to the robustness of kangax’s solution. I feel like I may have stooped a little too low in this solution, but I think that I may be justified in doing so:
(function () {
var toString = Object.prototype.toString,
strArray = Array.toString();
Array.isArray = Array.isArray || function (obj) {
return typeof obj == "object" && (toString.call(obj) == "[object Array]" || ("constructor" in obj && String(obj.constructor) == strArray));
}
})();
This runs through several checks for performance reasons; if the object’s internal [[Class]] is Array, then the extra checks aren’t necessary. The reason I feel like I’m stooping low here is that it relies on Function.prototype.toString(), a method which is defined in the spec as follows:
An implementation-dependent representation of the function is returned. This representation has the syntax of a FunctionDeclaration. Note in particular that the use and placement of white space, line terminators, and semicolons within the representation String is implementation-dependent.
Currently, all known implementations return a string in the format "function Array() { [native code] }", although the white-space within this returned string varies between those implementations, notably Internet Explorer’s has line breaks and indentation whereas others are inline. The real problem is that the string [native code] appears nowhere in the spec, so this could be changed at any given time to anything else at all. However, I can rest easy knowing that this code is safe and robust for the following reasons; a) we know that Array.toString() can’t be spoofed, because function Array () { [native code] } will throw a syntax error and, most importantly, b) we know that all current implementations of Function.prototype.toString() behave in this manner and if a future implementation changes it doesn’t matter because Array.isArray() will be present and used instead.
Potential Solution #2
Really, this is just the same solution, but with a bit of conditional compilation magic that means all other browsers can comfortably use Object.prototype.toString() while only IE will be targeted by our Function.prototype.toString(). With this, we can put to rest any fears that our code will have any unexpected repercussions in other browsers, with the added benefit that it should run faster because less checks are necessary:
(function () {
var toString = Object.prototype.toString,
strArray = Array.toString(),
jscript = /*@cc_on @_jscript_version @*/ +0;
// jscript will be 0 for browsers other than IE
if (!jscript) {
Array.isArray = Array.isArray || function (obj) {
return toString.call(obj) == "[object Array]";
}
}
else {
Array.isArray = function (obj) {
return "constructor" in obj && String(obj.constructor) == strArray;
}
}
})();
Potential Solution #3
Let’s face it, people aren’t keen on using conditional compilation. But the only other solution I could think of would be to create a new context (window or iframe), create an array in it and pass it back to the current context, then test if Object.prototype.toString() gives us "[object Object]". If it does, then we can flag that we need to use our workaround. The major downside here is that creating a new context is a pain – it would be slow and, if you’re using a frame, you’d have to add it to the document before being able to get its context. The fastest solution I could think of is to use IE’s proprietary createPopup(), but I’m only speculating that this would be faster and the solution uses a form of… eval.
(function () {
var popup,
toString = Object.prototype.toString,
strArray = Array.toString(),
hasNoClass = false;
if (typeof window.createPopup == "object" && Function.prototype.toString(createPopup).slice(0,8) == "function") {
popup = window.createPopup();
popup.document.parentWindow.execScript("window.test = [];");
hasNoClass = toString(popup.document.parentWindow.test) == "[object Object]";
}
if (hasNoClass) {
Array.isArray = function (obj) {
return "constructor" in obj && String(obj.constructor) == strArray;
}
}
else {
Array.isArray = Array.isArray || function (obj) {
return toString.call(obj) == "[object Array]";
}
}
})();
As usual, there’s a corresponding answer on Stack Overflow. Any thoughts? Please use the comments below.