An isArray test (and IE bugs)
- From: "Richard Cornford" <Richard@xxxxxxxxxxxxxxxxxxx>
- Date: Wed, 14 Jan 2009 23:48:16 -0000
While reading the ECMA spec (actually a draft for 3.1 but the algorithm is the same in 3) it occurred to me that the algorithm for the Array - concat - method offered a way in for a frame independent 'isArray' test function that could be fully sanctioned by ECMA 262. I have never had much use for such a test myself but the question has come up in the past and the possibilities should be considered.
There have been many versions of 'isArray' tests but to date none have been reliable, cross-frame and sanctioned by the ECMAScript specification (in the sense of being tests that the specification required to have correct outcomes).
The best to date has employed what is apparently to be called the "The Miller Device" (after Mark Miller, who thought it up). It uses the specified characteristics of - Object.prototype.toString - and (in this context) would take the form:-
function isArray ( obj ) {
return Object.prototype.toString.call(obj) === "[object Array]";
}
The - Object.prototype.toString - method is specified as taking the internal [[Class]] property of an object and creating/returning a string by inserting it between '[object ' and ']'. Arrays are required to have a [[Class]] of "Array" and all other built-in/native objects are required to have other values in their [[Class]] properties.
The problem with this method (and thought it is obvious I have not yet seen anyone point it out) is that while ECMA 262 does specify the precise value of [[Class]] properties for all built-in and native objects it does not (and could never) specify that value for host objects. The spec (ECMA 262 3rd Ed, Section 8.6.2) says: "The value of the [[Class]] property of a host object may be any value, even a value used by a built-in object for its [[Class]] property". Thus, it is entirely within the specification for any built in object to have a [[Class]] property, and for the value of that property to be "Array" (or "Function", which is significant when this method is applied to 'isFunction' tests). Thus, an object that has a [[Class]] property with a value that is not 'Array' must not be an Array, but an object with a [[Class]] property that is "Array" might be an Array _or_ some host object.
Now, step 4 in the algorithm for the - Array.prototype.concat - method reads:-
4. If E is not an Array object go to step 16.
-, which requires ECMAScript implementations to be able to discriminate between an object being an Array and its not being an Array. And if the implementation must be able to make that discrimination then it should be possible to exploit that ability to make the same discrimination for ourselves.
The implications of step 4 in the - concat - algorithms are that if you pass an object as an argument to the - concat - method, if that object is an Array then its 'array index' members are added to a new array created by the method, and if it is not an array then the object itself is added to that new array. Thus the test would be to pass the test subject as an argument to the - concat - method called on an empty array.
If the resulting array's length was anything but 1 then the test subject must have been an Array (an array with a length other than 1). If the resulting array's length was 1 but the only value in that array was not the test subject then the test subject also must have been an Array.
Having identified all arrays with lengths that are not 1 and all arrays that have a length of 1 but do not have themselves as their only element, the remaining task is to discriminate the one remaining Array possibility form all other objects.
The remaining Array possibility would be an Array along the lines of:-
var testAr = []
testAr[0] = testAr;
- but a non-Array along the lines of:-
var testObj = {length:1};
testObj[0] = testObj;
- is also a possibility. So the remaining testing checks to see if the test subject 'looks' like it might be an Array (in the form of) the one element Array above, and then uses the Array's special [[Put]] method to make the final discrimination for that last case.
So my proposed 'isArray' function goes:-
function isArray(a){
var ar, ret = false;
if(
(typeof a == 'object')&&
(a)&&
(typeof a.length == 'number')
){
ar = [].concat(a);
if(
(ar.length == 1)&&
(ar[0] === a)
){
if(
(a.length == 1)&&
(a[0] === a)&&
(typeof a.hasOwnProperty == 'function')&&
(a.hasOwnProperty('length'))&&
(a.hasOwnProperty('0'))
){
a.length = 0;
if(a[0] != a){
a[0] = a;
ret = (a.length === 1);
}
a.length = 1;
}
}else{
ret = true;
}
}
return ret;
}
- (There is a fully commented version at the end of this post.)
For a host object to pass this test (return true) it would have to have a '0' property (itself, not inherited) that referred to itself, a numeric length property with the value 1 and be such that assignments to its - length - property had Array-like side effects, and also that assigning to its '0' property had array-like side effects. A possibility but if that very unusual host object does exist anywhere it probably can be safely treated as an array.
There remains the possibility of encountering a host object that passes the earlier tests and then throws an exception during later tests. It is theoretical possibility, but I don't think may host objects are going to be getting past the - (a[0] === a) - line as having a '0' property that refers to itself would be an extremely unlikely characteristic of any object.
Despite this test being sanctioned by ECMA 262 3rd Ed. testing it did expose an implementation bug in IE browsers (6 and 7). One obvious test was to define the - isArray - function in a page that contained an IFRAME and have the page loaded in the frame pass the function arrays that 'belonged' to that frame (through - top.isArray(testSubject); -). Doing that worked fine (on IE Opera, Safari, Firefox and Chrome (and if it did not work that would be an implementation bug)). The next obvious test was replace the IFRAME with a - window.open - call and have the page loaded in the new window call the test function as - opener.isArray(testSubject); -, and on IE that call failed to correctly identify Array (asserting false for array arguments).
Looking into this it seems that inter-window exchanges of objects get wrapped in additional objects. Presumably this is for security reasons, but you get some strange effects from it, such as the - hasOwnProperty - method of the Arrays passed in from the external widow produce 'object' when tested with - typeof -, while the 'local' array's methods produce 'function'.
So does this mean that, although it has no basis in ECMA 262 for Array/host object discrimination, the "The Miller Device" is the better test? Unfortunately it does not because the "The Miller Device" is victim of the same bug as -
Object.prototype.toString.call(arrayFromAnotherWindow)
- returns "[object Object]' in IE 6 and 7 (with similar implications for the "The Miller Device" in 'isFunction' testing caused by the same 'wrapping' of 'remote' objects)
It is probably a good thing for everyone that pop-up blocking has virtually eliminate cross-window scripting.
Richard Cornford.
// Fully Commented - isArray - source:-
function isArray(a){
var ar, ret = false;
/* If - typeof a - is not 'object' then - a - cannot be an Array.
The - null - value also has a typeof of 'object' so a type-
converting test excludes null.
Arrays have numeric - length - properties so if - a - does not
have such a property then it is not an Array.
*/
if(
(typeof a == 'object')&&
(a)&&
(typeof a.length == 'number')
){
/* The algorithm for the - concat - method of an array
includes a step 4 which reads:-
4. If E is not an Array object go to step 16.
- which is interesting because it expects an implementation
to 'know' that an object is an Array, rather than testing
some characteristic of that object to determine whether it
looks like an Array. Thus this step needs to be able to
avoid issues such as those with - x instanceof Array -,
which does not work across frames (as the identity of the
Array constructor (or rather its - prototype -) is not
necessarily the same in each frame) and it also produce
a positive assertion with objects that have Arrays on
their prototype chains (such objects would also inherit
all the array properties and method so duck-typing would
be fooled by them).
The effect of - concat - is that if its argument is not
an array then its single value is added to the returned
array, and if its argument is an array then each of that
array's ('array index') members are added to the returned
array in turn.
So if you - concat - the subject of the test to an empty
array and the result has a length that is not one then
the subject of the test must be an array.
*/
ar = [].concat(a);
/* However, if the - length - is one then the test subject
may still be an Array. If it is an Array then it must be
a one element array, and its first element will be the
one at index zero in the new array. So if the element at
index zero in the new array is not the test subject then
the test subject must be an array.
*/
if(
(ar.length == 1)&&
(ar[0] === a) //ar[0] may be primative so must use ===
){
/* If the test subject were an Array then it could only
be a one element array with a reference to itself as
its only member. Thus if its length is not one it
is not an Array, and if its zero indexed element is
not itself (the object) then it is not an Array.
Also, if the test subject is an Array then its -
length - and its - 0 - properties must be its own.
Note: an ES 3.1 version of this test would have to
verify that neither of the - length - or - 0 -
properties had 'getters' or 'setters' (else the
modifications later may have unknowable side effects
that could not be reversed here). For an Array
neither should.
*/
if(
(a.length == 1)&&
(a[0] === a)&& //a[0] may be primative so must use ===
(typeof a.hasOwnProperty == 'function')&&
(a.hasOwnProperty('length'))&&
(a.hasOwnProperty('0'))
){
/* The odds of getting here are extremely low. The
only possibilities are a single element array that
has a reference to itself as its single element or
an object with a '0' property that is a reference
to itself and a - length - property that is
numeric one.
If the test subject is an array then assigning zero
to its - length - will have the side effect of
deleting its zero indexed element. If it is not an
Array then that assignment would not have that
side effect.
The property modification on the test subject is
acceptable here because we have narrowed down to
the real edge cases, have determined that the
properties that will be modified are properties
of the test subject itself, and know precisely
which values those properties started with and
so can safely restore them.
*/
a.length = 0;
/* Previously - (a[0] === a) - was tested to be true,
so now if it is false we have the side effects that
are expected of a - length - assignment on an
Array, and then if - (a[0] != a) - is false the
test subject cannot be an Array.
*/
if(a[0] !== a){
/* The object has been modified so it should be
returned to its original state, and so the
deleted zero index property is resorted to
its original state.
*/
a[0] = a;
/* This assigning to the zero property should
also have a side effect on the - length -,
if it does not then the test subject is not
an Array. It is an Array if the length is
back to one, else it cannot be an Array.
*/
ret = (a.length === 1);
}
/* If we did not pass through the previous - if -
block (or the subject did not turn out to be an
Array in that block) then the subject's - length
- needs to be re-set to its original value,
which was one. This is harmless if we did
determine the subject to be an Array.
*/
a.length = 1;
}
}else{
ret = true;
}
}
return ret;
}
.
- Follow-Ups:
- Re: An isArray test (and IE bugs)
- From: Lasse Reichstein Nielsen
- Re: An isArray test (and IE bugs)
- From: Matt Kruse
- Re: An isArray test (and IE bugs)
- From: Peter Michaux
- Re: An isArray test (and IE bugs)
- From: kangax
- Re: An isArray test (and IE bugs)
- From: Thomas 'PointedEars' Lahn
- Re: An isArray test (and IE bugs)
- Prev by Date: Re: jQuery 1.3 Released
- Next by Date: Re: reserved words for prototype and/or scriptaculous?
- Previous by thread: JavaScript and JSON
- Next by thread: Re: An isArray test (and IE bugs)
- Index(es):
Relevant Pages
|