jQueryはどうやって型判定してるのか

  • 27
    Like
  • 0
    Comment
More than 1 year has passed since last update.

jQueryにはtype()という型判定のメソッドがあります。どういう判定をしているのか気になったので、調べてみました。

とりあえずコード

主要な部分だけ抜き出しました。

jquery-3.1.1.js

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

さらに、nullundefinedを比べたときは、==ならtrue===ならfalseを返すという性質があります。

null == undefined; // true
null === undefined; // false

この性質を利用し、「objnullまたは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というプロパティがなければ、生成元であるObjectprototypeからtoStringというプロパティを引っ張りだしてきます。)

このtoString()というメソッドはObject以外の主要オブジェクトにも存在しますが、動作はバラバラです。
Object.prototype.toString() - JavaScript | MDN

そこでcall()が出てきます。call()は全ての関数に付いているメソッドで、method.call(arg)とすると、argmethod()というメソッドがあるかどうかに関わらず、あたかもargのメソッドとしてmethod()を呼び出したかのような動作になります。
ObjecttoString()は「オブジェクトを表す文字列を返す」という働きを持っているので、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なしに実装するとしたらこう

type.js
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;
  };
})();
test.js
type("hoge"); // "string"
type([]); // "array"
type(/s/); // "regexp"
type(new Date()); // "date"
type(function(){}); // "function"
type(new String("fuga")); // "string"