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

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

## とりあえずコード

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

```js: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つがあります。どう違うのかというと、左右の型が違うときに、`==`は型を変換し、`===`は型を変換しません。

```js:
3 == "3"; // true
3 === "3"; // false
0 == false; // true
0 === false; // false
```

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

```js:
null == undefined; // true
null === undefined; // false
```

この性質を利用し、「`obj`が`null`または`undefined`なら」という処理をしています。
最後に`return obj + "";`としているのは、空文字を足すことで文字列として値を返すためです。

```js:
jQuery.type(undefined); // "undefined"
jQuery.type(null); // "null"

// 値がない場合も "undefined" が返される
jQuery.type(); // "undefined"
jQuery.type(window.notDefined); // "undefined"
```

### `typeof obj`

JavaScriptには一応、`typeof`という型を返す演算子が存在するのですが、実装はひどいことになっています。

```js:
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](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof)
まあ`new String()`とかはオブジェクトであることにはあるのでしょうがない気もしますが、`null`が`"object"`だったり配列が`"object"`だったりというのは使いづらいわけです。
そこで、`typeof`で判断できるものを除き、三項演算子による分岐で別の処理を行います。なのでこの時点で以下のようになります。

```js:
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()`というメソッドがあります。

```js:
({}).toString(); // "[object Object]"

// Object.prototypeでも同様
Object.prototype.toString(); // "[object Object]"
```

（`prototype`というのは親から子に受け継がれるプロパティみたいなものなので、`{}`に`toString`というプロパティがなければ、生成元である`Object`の`prototype`から`toString`というプロパティを引っ張りだしてきます。）

この`toString()`というメソッドはObject以外の主要オブジェクトにも存在しますが、動作はバラバラです。
[Object.prototype.toString() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/toString)

そこで`call()`が出てきます。`call()`は全ての関数に付いているメソッドで、`method.call(arg)`とすると、`arg`に`method()`というメソッドがあるかどうかに関わらず、あたかも`arg`のメソッドとして`method()`を呼び出したかのような動作になります。
`Object`の`toString()`は「オブジェクトを表す文字列を返す」という働きを持っているので、`call()`を使ってこれを別のオブジェクトから呼び出してみると……

```js:
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`で判断しきれなかったものを判断できます。

```js:
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なしに実装するとしたらこう##

```js: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;
  };
})();
```

```js:test.js
type("hoge"); // "string"
type([]); // "array"
type(/s/); // "regexp"
type(new Date()); // "date"
type(function(){}); // "function"
type(new String("fuga")); // "string"
```