欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

`instanceof` considered harmful (or how to write a robust `isArray`) javascriptprototype 

程序员文章站 2022-07-06 15:41:30
...

 

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 – usinginstanceof 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 aboutgetClass and its possible usage in isArrayisDate 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!