Few days ago one jQuery discussion, “blossomed” around good old isObject() / isArray(). One of few never ending javascript themes.
Yet another typeof related discussion ? That was my initial reaction. A very stubborn little issue, especially considering that javascript is typeless language. Which in reality is a misnomer. It is impossible to have true and pure “no types involved” language.
Rather there is a type system behind which gives illusion, that types do not exist. It transparently coerces values from ‘type’ A to ‘type’ B. This is usually implemented with the help of an “root type”. An object that encapsulates type issues. Sometimes this is called : meta type. Type of types.
For example in VBScript we have VARIANT. In JavaScript there is a slight confusion, unfortunately. JavaScript implementations have something one might call: “root type”. It is implemented and called: “Object”. But. ECMA spec is not mirroring this, explicitly . So in JavaScript we have automatic type coercion rules. But they are not specified in simple object oriented terms. That is : global JavaScript object , called “Object” is not specified as a meta type. For example intinsic number type, is not specified as a “kind-of-a” Object, that is an object that inherits from “Object”.
And then, there is unfortunate typeof operator. And its even more unfortunate instanceof sibling.
Instead of indulging into an lengthy explanation on why are they both problematic, I will show this code:
var A = [1,2,3] ; A instanceof Array /*true*/ typeof Array /*function*/
Well, there is no any other so (seemingly) simple language like JavaScript, which can confuse a good natured beginer, so much. Here she is bound to ask you: How can A be an “intanceof” Array, if typeof Array is clearly a “function” ? Well, in JavaScript it certainly is not, but then it certainly appears to be. Ugh, imagine explaining this to someone who knows nothing of, object orientation, C++, or C# or Java and who is learning programming through JavaScript. Or this little ‘number’:
var color1 = new String("green");
color1 instanceof String; // returns true
var color2 = "coral";
color2 instanceof String; // returns false (color2 is not a String object, it is a string literal)
Ok, these are the problems, we need some solutions. Let me jump stright back to the point. Beginners or not, JavaScript developers have simply given up on those two above. Each and every javascript library has somewhere inside at least two methods, which are answering a simple (?) question : is this thing an Array or is this thing an Object ?
This is the barre minimum, since typeof returns “object” for both Objects and Arrays. Is this supposed to be or not, is a moot point, which can be properly answered only if we think of the implementation of the language. Then we will realise that typeof sometimes “reveals” the parent object and sometimes not.
typeof {} // returns: "object"
typeof [] // returns: "object"
typeof "" // returns: "string", because "" is a string literal
typeof new String() // returns "object"
typeof 1 // returns: "number", 1 is intrinsic type
typeof new Number(1) // return "object"
Most importantly type of array is returned as the type of its (real) prototype: “object”. Why is this and how is this is discussed countless of times elsewhere. What we need is solution for a robust and feasible way to encapsulate this issues and “forget” about this. This is done usually in the form of two simple (or not, it depends) functions :
var isFunction = function(obj) {
return Object.prototype.toString.call(obj) === "[object Function]";
}
var isArray = function(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
}
Nice and simple and works. Well, almost. In IE this isFunction, will not work.
// alert() is a function, yes ? isFunction(alert) /* returns : false in IE */
Object.prototype.ToString(obj) , has “special” behavior if given an argument. It returns a full ECMA “Class” name. An “type descriptor”, of the argument given. In IE, isFunction() has a problem.
isArray() has no problems. It works. For the time being. What do I mean by this? I mean that I do not like this implementation. This implementation depends on well behaved browsers. This means, that we expect each and every browser to return these exact strings . In the present and future. Including some new and peculiar little browser on some new mobile platform? Hm, I would not bet on this. Some will, but I would not.
Towards the Solution
I think that first of all conceptually it has to be clear that “type” is overloaded term here. It seems we will be able to decouple ourselves from javascript type issues. First of all (as ever) we have to sort out the terminology.
Object.prototype.toString.call(o) returns what I shall call:
“type descriptor”
Type descriptor format is well known. I shall formalise it as :
<type descriptor> := "[ object " + <role> + "]" <role> := "Argument" | "Object" | "Array" | "Error" | "RegExp" | "Number" | "String" | "Math" | "Function" | "JSON"
(“Argument” and “JSON” are part of ECMA 5.) Type descriptor is a string naming the type and the role of the object in JavaScript. The key part here is: role .
We use this to differentiate typeof name of an object from his role.
The idea is to use the Role concept to differentiate between the type and the role.
Example: [] is an “object” whose role is to be an “Array”.
JavaScript “typeof” operator returns the “type name”, NOT the “role name”. There is no “roleof” operator in JavaScript. There is no operator in JavaScript, which would simply return a proper type descriptor on an array. There is only Object.prototype.toString() :
Object.prototype.toString.call([]) === "[object Array]"; // full type descriptor string // [] object name is : "object" // [] object role is : "Array"
object descriptor= type + role
What are we actually interested in are roles, not types of JavaScript objects. We want to know is something an Array or Date or RegEx. In 99.99% of cases we do not want to know that they are all objects. We know that already. Let me present to You, a small framework based on this discussion.
/*
$Revision: 5 $
$Date: 8/11/09 16:46 $
Copyright (c) 2009-2010 by DBJ.ORG
*/
(function() {
dbj || (dbj = {});
var globals = [
new Array, new Boolean, new Date, new Error, new Function, Math,
new Number, new Object, new RegExp, new String
];
if ("object" === typeof Arguments) // ECMA5 Arguments object
globals[globals.length] = Arguments;
if ("object" === typeof JSON) // add JSON if exist as inbuilt object
globals[globals.length] = JSON;
dbj.role = {
name: function(o) {
///<summary>
/// NOTE: for IE DOM objects function bellow will return "object"
/// for methods. example: window.alert returns "object"
///</summary>
return Object.prototype.toString.call(o).match(/\w+/g)[1];
},
names: {
///<summary>
/// distinctive role names, and their unique role id's
/// { "Array" : 0, ... }
///</summary>
}
};
/// generate dbj.role.names object, with distinctive role names
/// and their type ID's
var name_ = "";
for (var j = 0; j < globals.length; j++) {
name_ = dbj.role.name(globals[j]);
dbj.role.names[name_] = j;
};
/// generate dbj.role.is<role name>() checks
/// we compare role id's bellow, not names
/// so we compare numbers, not strings
for (var j in dbj.role.names) {
dbj.role["is" + j] = Function(
"o",
"return dbj.role.names[dbj.role.name(o)] === dbj.role.names['" + j + "'];");
};
})();
What we have here generated, is first the “repository” of roles and their id’s.
dbj.role.names
/*
{ Array: 0, Boolean: 1, Date: 2, Error: 3, Function: 4, Math: 5, Number: 6, Object: 7, RegExp: 8, String: 9, JSON: 10 }
*/
Then, we have generated “is” function for each role defined. For example, for Function we have :
dbj.role.isFunction = function (o) {
return dbj.role.names[dbj.role.name(o)] === dbj.role.names['Function'];
}
Here we actually compare the ID of the role “Function”, which above is : 4, with the ID of the computed role of the argument “o”.
Ok, but really: Why ‘Role’, why not ‘Class’ ?
Because Class is very overloaded term. Especially if one deals with several languages in the same time. ECMA 5 is using the term ‘Class’, for one of its ‘internal property’-es. The “spec” says:
The value of the [[Class]] internal property is defined by this specification for every kind of built-in object. The value of the [[Class]] internal property of a host object may be any String value except one of “Arguments”, “Array”, “Boolean”, “Date”, “Error”, “Function”, “JSON”, “Math”, “Number”, “Object”, “RegExp”, and “String”. The value of a [[Class]] internal property is used internally to distinguish different kinds of built-in objects. Note that this specification does not provide any means for a program to access that value except through Object.prototype.toString()
This is excellent news! String values : “Arguments”, “Array”, “Boolean”, “Date”, “Error”, “Function”, “JSON”, “Math”, “Number”, “Object”, “RegExp”, and “String”, are reserved by the latest language spec. This is what we use as role names, and this is what comes out of Object.prototype.toString() .
But, I beg to differ on using the term “Class”, in this context. Simply because “class” as used here is not what “class” means in OOD, OOA, C++, C# or Java. There is no “class hierarchy” behind an JavaScript implementation. There are no classes in JavaScript. Just objects and prototypes.
Also, the “Class”, is just used and not defined in ECMA 5 spec. I have choosen the term “role”, which I think, is much closely related to the reality of the JavaScript nature. So, what is ECMA 5 internal property “Class” is what we call here: the “Role”. And the reserved values of it, is what we use as role names.
What about IE ?
I hope I have presented here an simple and useful mechanism, that does its job well. Especially if ubiquotusly used, across all the browsers. This mechanism, also seems robust and future proof.
From now on I will use this each and every time I need objects Role related information in my JavaScript code. Which is almost always.
Also here we are facing IE browser differences, head on. Because of the simple reason: in IE, methods on DOM nodes are treated as objects. Are they “treated” or “implemented” or “thought of” , does not matter. What matter is that in IE we have this situation :
"object" === typeof alert ; // true in IE dbj.role.isFunction( alert ) ; // false in IE
Ok, ok. I am an professional. I will serve my customers. I will not try to educate them
Solutions for IE, are certainly doable, and perhaps not very elegant. For that please see my next post.
–DBJ