`instanceof` considered harmful (or how to write a robust `isArray`)
Checking types in Javascript is well known as a pretty unreliable process.
Good old typeof
operator is often useless when it comes to certain types of values:
typeof null; // "object"
typeof []; // "object"
People often expect to see something like “null” in the former check and something like “array” in the latter one.
Fortunately, checking for null
is not that hard, despite useless typeof
, and is usually accomplished by strict-comparing value to null
:
value === null;
Checking for arrays, on the other hand, is a somewhat tricky business. There are usually two schools of thought – using instanceof
operator (or checking object’s constructor
property) and the-duck-typing way – checking for presence (or types) of certain set of properties (which are known to be present in array objects).
Obviously, both ways have their pros and cons.
1) `instanceof` operator / `constructor` property
instanceof
operator essentially checks whether anything from left-hand object’s prototype chain is the same object as what’s referenced by prototype
property of right-hand object. It sounds somewhat complicated but is easily understood from a simple example:
var arr = [];
arr instanceof Array; // true
This statement returns `true` because Array.prototype
(being a prototype
property of a right-hand object) references the same object as an internal [[Prototype]] of left-hand object ([[Prototype]] is “visible” via arr.__proto__
in clients that have __proto__
extension). An alternative constructor
check, which I mentioned earlier, would usually look like:
var arr = [];
arr.constructor == Array; // true
Both instanceof
and constructor
look very innocent and seem like great ways to check if an object is an array. If I remember correctly, latest jQuery is using constructor
:
An excerpt from jQuery (rev. 5917):
...
isArray: function( arr ) {
return !!arr && arr.constructor == Array;
}
...
The problems arise when it comes to scripting in multi-frame DOM environments. In a nutshell, Array objects created within one iframe do not share [[Prototype]]’s with arrays created within another iframe. Their constructors are different objects and so both instanceof
and constructor
checks fail:
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]
// Boom!
arr instanceof Array; // false
// Boom!
arr.constructor === Array; // false
This “problem” was mentioned by Crockford as far as back in 2003. Doug suggested to try duck-typing and check for a type of one of the Array.prototype
methods – e.g.:
typeof myArray.sort == 'function'
Exactly for these reasons Javascript authors often resort to a second approach:
2) Duck-typing
We’ve been using it in Prototype.JS for quite some time now. Dean Edwards was using it in its base2, last time I looked at it.
An excerpt from Prototype.js (v. 1.6.0.3):
function isArray(object) {
return object != null && typeof object === "object" &&
'splice' in object && 'join' in object;
}
By “fixing” multi-frame “problem”, this naive approach fails short in some of the trivial cases. If you were ever to have an object with splice
and join
properties, Object.isArray
would obviously detect that object as being an Array:
var testee = { splice: 1, join: 2 };
Object.isArray(testee); // true
Back in June, I was reading ECMA-262 specs and noticed that there was an easy way to get value of an internal [[Class]] property that every native object has. Object.prototype.toString
was defined like so:
Object.prototype.toString( )
When the toString method is called, the following steps are taken:
1. Get the [[Class]] property of this object.
2. Compute a string value by concatenating the three strings “[object ", Result (1), and "]“.
3. Return Result (2)
Contrary to Function.prototype.toString
which is implementation dependent and is NOT recommended to be relied upon, Object.prototype.toString
has a clearly defined behavior for all native objects.
15.3.4.2 Function.prototype.toString()
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.
Just as a fun exercise, I wrote a simple __getClass
method, put it into an “experimental” folder and forgot about it : )
function __getClass(object) {
return Object.prototype.toString.call(object)
.match(/^\[object\s(.*)\]$/)[1];
};
A couple of weeks ago, though, someone created a ticket for Prototype.js – proposing an Object.isDate method. An implementation used constructor
check and so was vulnerable to cross-frame issues. This is when I remembered about getClass
and its possible usage in isArray
, isDate
and other similar methods.
Specs mention that:
15.4.2.1 new Array([ item0[, item1 [,...]]])
…
The [[Class]] property of the newly constructed object is set to “Array”.
…
This means that creating isArray
function could not be simpler than:
function isArray(o) {
return Object.prototype.toString.call(o) === '[object Array]';
}
The solution is not dependent on frames (since it checks internal [[Class]]) and is more robust than duck-typing approach. I have tested it on a handful of browsers (including some archaic and mobile ones) and was happy to find that all of them are indeed compliant in this regard.
Let’s hope this little “trick” serves as a remedy to cross-frame issues that authors struggle to find workarounds for : )
Happy new year!
Did you like this? Donations are welcome
comments powered by Disqus