jQueryにはtype()という型判定のメソッドがあります。どういう判定をしているのか気になったので、調べてみました。
とりあえずコード
主要な部分だけ抜き出しました。
var class2type = {};
var toString = class2type.toString;
jQuery.extend({
/* ... */
type: function( obj ) {
if ( obj == null ) {
return obj + "";
}
// Support: Android <=2.3 only (functionish RegExp)
return typeof obj === "object" || typeof obj === "function" ?
class2type[ toString.call(obj) ] || "object" :
typeof obj;
},
/* ... */
});
// Populate the class2type map
jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
function( i, name ) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
} );
extend()
というのはjQueryオブジェクトにメソッドやプロパティを追加するメソッド、each()
というのは配列のそれぞれの要素に対して同じ処理をするメソッドです。
解説
obj == null
まずjQuery.type()
に判定したい値obj
が渡されます。
はじめは、obj == null
で分岐しています。ポイントは、比較が厳密な比較ではないところです。
JavaScriptの比較演算子のうち、値が同じかどうか比べる演算子は、==
と===
の2つがあります。どう違うのかというと、左右の型が違うときに、==
は型を変換し、===
は型を変換しません。
3 == "3"; // true
3 === "3"; // false
0 == false; // true
0 === false; // false
さらに、null
とundefined
を比べたときは、==
ならtrue
、===
ならfalse
を返すという性質があります。
null == undefined; // true
null === undefined; // false
この性質を利用し、「obj
がnull
またはundefined
なら」という処理をしています。
最後にreturn obj + "";
としているのは、空文字を足すことで文字列として値を返すためです。
jQuery.type(undefined); // "undefined"
jQuery.type(null); // "null"
// 値がない場合も "undefined" が返される
jQuery.type(); // "undefined"
jQuery.type(window.notDefined); // "undefined"
typeof obj
JavaScriptには一応、typeof
という型を返す演算子が存在するのですが、実装はひどいことになっています。
typeof undefined; // "undefined"
typeof null; // "object" (←!?)
typeof 42; // "number"
typeof "hello"; // "string"
typeof true; // "boolean"
typeof {a:1}; // "object"
typeof [1,2,3]; // "object" (←!?)
typeof new String("abc"); // "object" (←!?)
typeof new Boolean(true); // "object" (←!?)
typeof new Date(); // "object" (←!?)
typeof function(){}; // "function"
typeof /s/; // "function" (ブラウザによって異なる)
typeof /s/; // "object" (ブラウザによって異なる)
typeof Symbol(); // "symbol" (ES2015で追加)
typeof 演算子 - JavaScript | MDN
まあnew String()
とかはオブジェクトであることにはあるのでしょうがない気もしますが、null
が"object"
だったり配列が"object"
だったりというのは使いづらいわけです。
そこで、typeof
で判断できるものを除き、三項演算子による分岐で別の処理を行います。なのでこの時点で以下のようになります。
jQuery.type("hoge"); // "string"
jQuery.type(-20); // "number"
jQuery.type(false); // "boolean"
jQuery.type(Symbol("fuga")); // "symbol"
toString.call(obj)
極めつけがtoString.call(obj)
です。Objectオブジェクトには、toString()
というメソッドがあります。
({}).toString(); // "[object Object]"
// Object.prototypeでも同様
Object.prototype.toString(); // "[object Object]"
(prototype
というのは親から子に受け継がれるプロパティみたいなものなので、{}
にtoString
というプロパティがなければ、生成元であるObject
のprototype
からtoString
というプロパティを引っ張りだしてきます。)
このtoString()
というメソッドはObject以外の主要オブジェクトにも存在しますが、動作はバラバラです。
Object.prototype.toString() - JavaScript | MDN
そこでcall()
が出てきます。call()
は全ての関数に付いているメソッドで、method.call(arg)
とすると、arg
にmethod()
というメソッドがあるかどうかに関わらず、あたかもarg
のメソッドとしてmethod()
を呼び出したかのような動作になります。
Object
のtoString()
は「オブジェクトを表す文字列を返す」という働きを持っているので、call()
を使ってこれを別のオブジェクトから呼び出してみると……
var toString = Object.prototype.toString;
toString.call("abc"); // "[object String]"
toString.call(34); // "[object Number]"
toString.call(true); // "[object Boolean]"
toString.call({}); // "[object Object]"
toString.call([]); // "[object Array]" (←!!)
toString.call(new String("fuga")); // "[object String]" (←!!)
toString.call(new Boolean(true)); // "[object Boolean]" (←!!)
toString.call(function(){}); // "[object Function]"
toString.call(/s/); // "[object RegExp]" (←!!)
toString.call(new Date()); // "[object Date]" (←!!)
toString.call(document.createElement("div")); // "[object HTMLDivElement]" (←!!)
// undefined と null はブラウザによっては "[object Window]"
toString.call(undefined); // "[object Undefined]"
toString.call(null); // "[object Null]"
toString.call(undefined); // "[object Window]" (古い実装)
toString.call(null); // "[object Window]" (古い実装)
toString.call(Symbol("piyo")); // "[object Symbol]" (ES2015で追加)
他にもいろいろと試してみるとわかるかと思います。
この方法によって、typeof
で判断しきれなかったものを判断できます。
jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
function( i, name ) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
} );
// class2type の中身↓
{
"[object Boolean]": "boolean",
"[object Number]": "number",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Date]": "date",
"[object RegExp]": "regexp",
"[object Object]": "object",
"[object Error]": "error",
"[object Symbol]": "symbol"
}
そしてtoString.call()
をキーにしてclass2type
から値を抜き取り(なかったら"object"
)、返して終わりです。
##jQueryなしに実装するとしたらこう##
var type = (function(){
var class2type = {};
var toString = class2type.toString;
"Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ").forEach(function(c) {
class2type[ "[object " + c + "]" ] = c.toLowerCase();
});
return function( obj ) {
return obj == null ? obj + "" :
typeof obj === "object" || typeof obj === "function" ?
class2type[ toString.call(obj) ] || "object" :
typeof obj;
};
})();
type("hoge"); // "string"
type([]); // "array"
type(/s/); // "regexp"
type(new Date()); // "date"
type(function(){}); // "function"
type(new String("fuga")); // "string"