JavaScriptにおける「データ型」の判定メモ

―令和元年のある日、とあるぽっぽこエンジニアが、先人たちのコードを読んでいて「？？」となったことがあったらしい。

「データ型の判定で、ここでは`typeof`じゃなくて`Object.prototype.toString.call()`が使われているけど、なぜだろう…？」
「ていうか、こいつらなにが違うん…？」

これは、そんな疑問からはじまった**JavaScriptにおけるデータ型の判定**についての探検をメモしたものである。


## データ型を判定する方法
- `typeof演算子`を使う
- `Object.prototype.toString.call()`を使う
- その他の方法


## typeof演算子を使う
JavaScriptの入門書とかによく書かれている方法…？データ型判定だとなんだかよく見るやつ。
ここで注意したいのは、[MDN の typeof](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof)に書かれているように

> 構文
> typeof 演算子の後に、オペランドを続けて書きます。
> typeof operand

> 引数
> operand は、オブジェクトもしくは primitive 型を返す式を表します。

なのである。

ほう…「オペランドは、オブジェクトもしくはプリミティブ型を返す式を表す」とな。

ちなみに、JavaScriptのデータ型は`オブジェクト型`と`プリミティブ型`の2つに大きくわけられるそう。

プリミティブ型：プロパティとメソッドを持たない単純なデータ（1 や 'Hello' や true）
オブジェクト型：プロパティとメソッドの集まり
な感じ。ざっくり。

typeof演算子で判別したらざっくりとこんな感じ↓↓↓

<table>
<tr><th>分類</th><th>データ型</th><th>概要</th><th>戻り値</th></tr>
<tr><td rowspan="7">プリミティブ型</td><td>Number</td><td>数値 / NaN / Infinity など</td><td>number</td></tr>
<tr><td>String</td><td>文字列</td><td>string</td></tr>
<tr><td>Boolean</td><td>true / false</td><td>boolean</td></tr>
<tr><td>null</td><td>null（値が存在しない）</td><td>object（※1）</td></tr>
<tr><td>undefined</td><td>undefined（未定義）</td><td>undefined</td></tr>
<tr><td>Symbol</td><td>ユニークな値を返す（識別子として使われる）</td><td>symbol</td></tr>
<tr><td>BigInt</td><td>任意精度の整数値</td><td>bigint</td></tr>
<tr><td rowspan="5">オブジェクト型</td><td>Array</td><td>連番が付けられた値の集合</td><td>object</td></tr>
<tr><td>Map</td><td>キーと値のセットからなる集合<br>（Objectと違いダイレクトに反復処理できる）</td><td>object</td></tr>
<tr><td>Set</td><td>重複しない値の集合</td><td>object</td></tr>
<tr><td>Object</td><td>キーと値のセットからなる集合</td><td>object</td></tr>
<tr><td>Function</td><td>function() {} / Math.round など</td><td>function（※2）</td></tr>
</table>
※1：nullがなぜ'object'判定に…？それはjsの仕様上、仕方のないことだそうで。
※2：関数オブジェクトだけは特別に'function'を返す。


だが、しかし、MDN曰く

> コンストラクター関数はすべて、関数コンストラクターを除いて、常に typeof 'object' になります

とのこと。
new演算子でインスタンス化すると、prototypeを参照できるオブジェクトになるので…って感じ。

よって、以下の判定はすべて`object`になってしまう。

```javascript
const hoge = new Number(10);
const fuga = new Boolean(true);
const piyo = new Date();

console.log(typeof hoge);
console.log(typeof fuga);
console.log(typeof piyo);
```


ここでちょっとややこしくなるが、`ラッパーオブジェクト`という存在に触れておきたい。（メモしておきたいだけ）

たとえば、

```javascript
console.log('ねこさん');
```
ってやったら単純に「ねこさん」が返ってくる。そりゃそうだ。

でも、

```javascript
console.log(new String('ねこさん'));
```
ってやると、なんて返ってくるのか…？（Google Chrome の console でやるのがおすすめ）

答えは…

```javascript
String {0: "ね", 1: "こ", 2: "さ", 3: "ん", length: 4, [[PrimitiveValue]]: "ねこさん"}
```

…はて？

簡単に説明すると
このようにプリミティブ型をnew演算子でインスタンス化すると、プリミティブ型のデータをラップしたオブジェクトとなる。これが`ラッパーオブジェクト`。

なので、`console.log(typeof new String('ねこさん'));`とかやると、ラッパーオブジェクトの名の通り、`object`の判定になる。

よって、以下の判定はすべて`object`。

```javascript
console.log(typeof new String('ねこさん'));
console.log(typeof new Number(10));
console.log(typeof new Boolean(true));
```
※`Symbol`と`BigInt`はそもそもnew演算子を使用できません。

ちなみに
インスタンス化せずに普通に文字列リテラルで生成したときは、プロパティとかにアクセスするときだけ一時的に裏側で`Stringクラス`の機能を持ったオブジェクトでラップする。
そうしてオブジェクトとしての機能へアクセスできるようになるそうだ。


…話を戻して、ここまで見てきて感じたと思うが、`typeof演算子`での判定ってなんかこう…ふわっとしている………。
配列とオブジェクトの区別できんやん…！とか。

じゃあ、もっと詳しく判定する方法へ。


## Object.prototype.toString.call() を使う
`Object.prototype.toString()`は`[object type]`（typeはデータ型）といった形で、いわゆるクラスを返してくれる。
よく見るようなサンプルコードで見た方が早いので↓↓↓

```javascript
const toString = Object.prototype.toString;

toString.call(1234);          // [object Number]
toString.call(Infinity);      // [object Number]
toString.call(NaN);           // [object Number]
toString.call('hoge');        // [object String]
toString.call(true);          // [object Boolean]
toString.call([]);            // [object Array]
toString.call(new Map());     // [object Map]
toString.call(new Set());     // [object Set]
toString.call({});            // [object Object]
toString.call(function() {}); // [object Function]

toString.call(new String('ねこさん')); // [object String]
toString.call(new Number(10));       // [object Number]
toString.call(new Boolean(true));    // [object Boolean]
toString.call(new Date());           // [object Date]

toString.call(Math); // [object Math]
toString.call(JSON); // [object JSON]

// ECMAScript5 から？
toString.call(null);      // [object Null]
toString.call(undefined); // [object Undefined]
```

`call()`メソッドは言うまでもないが
`function.call(引数)`で、引数があたかも自分のメソッドかのようにfunctionを使っちゃうメソッド。（雑）

上記の例だと、注意すべきは`NaN`くらい…？

あと、自作クラスから生成したインスタンスは`[object Object]`が返ってくる。
でも考えてみれば当たり前のような………


## その他の方法
その他の方法だと、`Array.isArray()`や`isNaN()`、`Number.isNaN()`あたりだろうか。

### Array.isArray()
これは単純に、渡された値が配列かどうかを真偽値で返してくれる。

```javascript
Array.isArray([]);          // true
Array.isArray(new Array()); // true
Array.isArray({});          // false
Array.isArray(new Map());   // false
Array.isArray(new Set());   // false
```

### isNaN() 、 Number.isNaN()
NaNの判定といえば`isNaN()`とか。
じゃあ、`isNaN()`と`Number.isNaN()`の違いってなんでござろう？

**`isNaN()`**
これは渡された引数を数値に変換してから判定するメソッド。
引数が数値へと強制変換された後にNaNであるかを確認するため、プログラム上NaN以外の数値を〜ってなところに、うっかり0や1に変換されてしまう空文字列や真偽値を渡してしまうと'false'と返される。要するにNaNじゃない数値だよ〜って判断してしまったりする。
例として以下のような判定がされてしまう………なんてことだ…。

```javascript
console.log(isNaN(NaN));       // true
console.log(isNaN(undefined)); // true
console.log(isNaN({}));        // true
console.log(isNaN('hoge'));    // true
console.log(isNaN(''));        // false
console.log(isNaN(true));      // false
console.log(isNaN(null));      // false
```

**`Number.isNaN()`**
比べてこちらは、引数が数値型であり、かつ、NaNであるもののみ`true`を返すメソッド。
いずれかを満たさなければfalseを返すので、心配なし。
ただ、ES6から新たに導入されたので環境次第では使えない場合も…。

```javascript
console.log(Number.isNaN(NaN));       // true
console.log(Number.isNaN(undefined)); // false
console.log(Number.isNaN({}));        // false
console.log(Number.isNaN('hoge'));    // false
console.log(Number.isNaN(''));        // false
console.log(Number.isNaN(true));      // false
console.log(Number.isNaN(null));      // false
```


## 最後に
結論としては、データ型判定は`Object.prototype.toString.call()`を使うのがベストかな…

まだまだぽっぽこエンジニアなので、間違いなどあればご教示いただけると幸いです。