RSS
 

Cross-context isArray and Internet Explorer

26 Oct

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.

 

Tags: , , ,

Leave a Reply

 

 
  1. kangax

    October 27th, 2010 at 5:05 am

    Interesting solution.

    I actually haven’t heard of IE problem at the time of the original post. Then a couple months later, Richard Cornford mentioned it in a comp.lang.javascript discussion. His theory was that IE seemed to have somehow “wrap” objects from other windows (perhaps for security purposes) in such way that their [[Class]] was always reported as “Object”. Curiously, this would only happen with objects from other “windows” (i.e. popups; as in those created by `window.open`) but not “iframes”.

    I’ve been meaning to revisit this (now-ubiquitous) technique mentioning IE weirdness (fixed in IE9 beta, btw, so yay!), as well as other thoughts on the subject, but never got around to it :)

    The problem with relying on function decompilation, as in your example, is that — as you mentioned — it’s non-standard, and so isn’t very reliable. Non-standard nature, coupled with _actual_ real-life deviations is the reason I’ve been always recommending against it. To give just a couple of examples, it was observed that some mobile browsers do not keep the source of the functions in order to save memory (returning something like /* source not available */ even for user-defined functions); older browsers tend to be less consistent with function representations (try Safari 2.x, for example — `Array + ”` returns “(Internal Function)” there); In Rhino, `Array + ”` results in “function Array() { [native code for Array.Array, arity=1] }”. I’ve listed few more here — http://perfectionkills.com/those-tricky-functions/

    While it’s certainly possible to spoof this kind of `Array.isArray` with an object like this:

    ({
    constructor: {
    toString: function() {
    return Array.toString();
    }
    }
    })

    …that’s probably more of an academic exercise rather than a real concern. The real concern is non-interoperable nature of relying on function-decompilation.

     
    • Andy

      October 27th, 2010 at 8:53 am

      @kangax: I did notice that it had been fixed in IE 9 whilst perusing your blog post on pr3 – http://perfectionkills.com/jscript-and-dom-changes-in-ie9-preview-3/ – it seems someone from the IE team read your post, it looks like all those issues are fixed in the beta.

      I had a thought after posting this solution last night, but it was around 2am so I decided it could wait until this morning. I’m not a big fan of conditional compilation normally, but I think it’s rather appropriate here. Using conditional compilation, we can target our code specifically at the versions of JScript affected (<=5.8). This also means that we can further optimize the performance and the code won’t fall foul of any differing implementations of Function.prototype.toString.

      I added a couple of other solutions to the post. The main goal is to get a consistent solution that works in all browsers, so personally, I’d go with the conditional compilation solution but I know a lot of developers won’t be keen on that idea.

       
  2. Radoslav Stankov

    November 3rd, 2010 at 5:24 pm

    I think something like this will speed the things in Array.isArray compatible browsers

    Array.isArray || (function(){
    // you current code
    })();

     
  3. scinos

    April 7th, 2011 at 7:01 pm

    I wonder how native functions that use Array as an argument behave to external context arrays. For example, Function.prototype.apply() or Array.prototype.concat().

    Can they be used to detect external context arrays? For example, using apply() inside a try/catch block to detect non-arrays, or concat() to a fixed array of well-know length and check final length (and another similar trick to detect zero length arrays).

     
    • Andy

      April 7th, 2011 at 7:18 pm

      The main problem is that try and catch blocks aren’t great for performance and concat would throw an error for an object that isn’t an array. Even the conditional compilation approach is more desirable than an exception handling-based solution, IMO.

       
      • scinos

        April 7th, 2011 at 10:41 pm

        [].concat(testedArray) won’t throw an error, afaik.

        I’m thinking in something like:
        function isArray(testArray) {
        //This is just an example, probably it doesn’t cover all cases
        var result1 = ([].concat(testArray).length === testArray.length);
        //Avoid {length: 1}?
        var result2 = ([].concat(testArray).concat(testArray).length === testArray.length * 2)
        return result1 && result2;
        }

        And yeah, using try/catch for detect an array is a dirty solution :) I just wonder if it will be technically possible.