30 June 2011

jQuery uses a clever bit of code to extract the type of a parameter.

$.type([]); //array

You can derive most of JavaScript’s weak types using the typeof operator. However, this often returns unexpected results:

typeof new Array(); 	//object -- wrong
typeof []; 		//object -- wrong
typeof 1; 		//number
typeof new Number(1); 	//object -- wrong
typeof NaN; 		//number -- wrong?
typeof null;		//object -- wrong
typeof true; 		//boolean
typeof new Boolean(true); //object -- wrong
typeof "x"; 		//string
typeof new String("x"); //object -- wrong
typeof new Date(); 	//object -- useless
typeof /x/;		//function -- wrong
typeof new RegExp("x");	//function -- wrong
typeof document;        //object -- useless

jQuery solved this problem by applying the Object.prototype.toString method which returns more predictable results–albeit in a strange format:

var type = Object.prototype.toString;
type.call(new Array());     //[object Array]
type.call([]);              //[object Array]
type.call(1);               //[object Number]
type.call(new Number(1));   //[object Number]
type.call(NaN);             //[object Number] --rly?
type.call(null);            //[object global] --huh?
type.call(true);            //[object Boolean]
type.call(new Boolean(true)); //[object Boolean]
type.call("x");             //[object String]
type.call(new String("x")); //[object String]
type.call(new Date());      //[object Date]
type.call(/x/);             //[object RegExp]
type.call(new RegExp("x")); //[object RegExp]
type.call(document);        //[object HTMLDocument]

However, this technique will not work with your home-grown types (Classes). For instance:

var type = Object.prototype.toString;
var X = function () {};
type.call(new X()); //[object Object]

X.prototype.toString = function () {
    return "\[object X\]";
};
type.call(new X()); //[object Object]

You probably expected to see “[object X]”.

Well, there’s another Object.prototype method called toLocaleString. It’s best known as a way to localize date objects. However, as the MDC points out here, it returns toString, so it can work on any object that has that method.

var type = Object.prototype.toLocaleString;
var X = function () {};
X.prototype.toString = function () {
    return "[object X]";
};
type.call(new X()); //[object X]

One more hurdle to overcome–just using toLocaleString will cause normal objects to return their string values instead of a type string:

var type = Object.prototype.toLocaleString;
type(new Array());  // ""
type.call(1);       // 1
type.call("x");     // "x"
type.call(/x/)      // "/x/"

By using toLocaleString only for “[object Object]” results, we can create a more convenient type method and get the expected results. I will also take the time to fix null, undefined, and NaN. Using a simple memoizer, I will collect and return the constructor in lower case like jQuery does:

var type = (function () {
    var memo = {},
    rword = /\w+/g,
    memoize = function (str) {
        if (typeof memo[str] === "undefined") {
            memo[str] = str.match(rword)[1].toLowerCase();
        }
        return memo[str];
    };
    return function (obj) {
        var t, s = Object.prototype.toString,
        ls = Object.prototype.toLocaleString;
        if (typeof obj === "undefined") {
            return "undefined";
        }
        if (obj === null) {
            return "null";
        }
        t = s.call(obj);
        switch (t) {
            case "[object Object]":
                t = ls.call(obj);
                break;
            case "[object Number]":
                if (ls.call(obj) === "NaN") {
                    t = "[object NaN]";
                }
                break;
            default:
        }
        return memoize(t);
    }
}());

var X = function () {};
X.prototype.toString = function () {
    return "[object X]";
};

console.log("type(new Array()) //", type(new Array()), type(new Array()) === "array");
console.log("type([]) //", type([]), type([]) === "array");
console.log("type(1) //", type(1), type(1) === "number");
console.log("type(new Number(1)) //", type(new Number(1)), type(new Number(1)) === "number");
console.log("type(NaN) //", type(NaN), type(NaN) === "nan");
console.log("type(null) //", type(null), type(null) === "null");
console.log("type(true) //", type(true), type(true) === "boolean");
console.log("type(new Boolean(true)) //", type(new Boolean(true)), type(new Boolean(true)) === "boolean");
console.log("type('x') //", type("x"), type("x") === "string");
console.log("type(new String('x') //", type(new String("x")),type(new String("x")) === "string");
console.log("type(new Date()) //",type(new Date()), type(new Date()) === "date");
console.log("type(/x/) //", type(/x/), type(/x/) === "regexp");
console.log("type(new RegExp('x')) //", type(new RegExp('x')), type(new RegExp('x')) === "regexp");
console.log("type(X) //", type(X), type(X) === "function");
console.log("type(new X()) //", type(new X()), type(new X()) === "x");
console.log("type(document) //", type(document), type(document) === "htmldocument");

EDIT 06-30-2011: Chrome reports Arguments as “[object Arguments]”, which you may think incredibly useful. However, other browsers return [object Object] and there remains no easy way to differentiate arguments from non-arguments objects. So, I recommend adding another case at the end of the switch statement above:

case "[object Arguments]":
    t = "[object Object]";
    break;

//and add this test:
var a = function () { return arguments; };
console.log("type(a()) //", type(a()), type(a()) === "object");


Discussion:

blog comments powered by Disqus