―令和元年のある日、とあるぽっぽこエンジニアが、先人たちのコードを読んでいて「??」となったことがあったらしい。
「データ型の判定で、ここではtypeof
じゃなくてObject.prototype.toString.call()
が使われているけど、なぜだろう…?」
「ていうか、こいつらなにが違うん…?」
これは、そんな疑問からはじまったJavaScriptにおけるデータ型の判定についての探検をメモしたものである。
データ型を判定する方法
-
typeof演算子
を使う -
Object.prototype.toString.call()
を使う - その他の方法
typeof演算子を使う
JavaScriptの入門書とかによく書かれている方法…?データ型判定だとなんだかよく見るやつ。
ここで注意したいのは、MDN の typeofに書かれているように
構文
typeof 演算子の後に、オペランドを続けて書きます。
typeof operand
引数
operand は、オブジェクトもしくは primitive 型を返す式を表します。
なのである。
ほう…「オペランドは、オブジェクトもしくはプリミティブ型を返す式を表す」とな。
ちなみに、JavaScriptのデータ型はオブジェクト型
とプリミティブ型
の2つに大きくわけられるそう。
プリミティブ型:プロパティとメソッドを持たない単純なデータ(1 や 'Hello' や true)
オブジェクト型:プロパティとメソッドの集まり
な感じ。ざっくり。
typeof演算子で判別したらざっくりとこんな感じ↓↓↓
分類 | データ型 | 概要 | 戻り値 |
---|---|---|---|
プリミティブ型 | Number | 数値 / NaN / Infinity など | number |
String | 文字列 | string | |
Boolean | true / false | boolean | |
null | null(値が存在しない) | object(※1) | |
undefined | undefined(未定義) | undefined | |
Symbol | ユニークな値を返す(識別子として使われる) | symbol | |
BigInt | 任意精度の整数値 | bigint | |
オブジェクト型 | Array | 連番が付けられた値の集合 | object |
Map | キーと値のセットからなる集合 (Objectと違いダイレクトに反復処理できる) |
object | |
Set | 重複しない値の集合 | object | |
Object | キーと値のセットからなる集合 | object | |
Function | function() {} / Math.round など | function(※2) |
だが、しかし、MDN曰く
コンストラクター関数はすべて、関数コンストラクターを除いて、常に typeof 'object' になります
とのこと。
new演算子でインスタンス化すると、prototypeを参照できるオブジェクトになるので…って感じ。
よって、以下の判定はすべてobject
になってしまう。
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);
ここでちょっとややこしくなるが、ラッパーオブジェクト
という存在に触れておきたい。(メモしておきたいだけ)
たとえば、
console.log('ねこさん');
ってやったら単純に「ねこさん」が返ってくる。そりゃそうだ。
でも、
console.log(new String('ねこさん'));
ってやると、なんて返ってくるのか…?(Google Chrome の console でやるのがおすすめ)
答えは…
String {0: "ね", 1: "こ", 2: "さ", 3: "ん", length: 4, [[PrimitiveValue]]: "ねこさん"}
…はて?
簡単に説明すると
このようにプリミティブ型をnew演算子でインスタンス化すると、プリミティブ型のデータをラップしたオブジェクトとなる。これがラッパーオブジェクト
。
なので、console.log(typeof new String('ねこさん'));
とかやると、ラッパーオブジェクトの名の通り、object
の判定になる。
よって、以下の判定はすべてobject
。
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はデータ型)といった形で、いわゆるクラスを返してくれる。
よく見るようなサンプルコードで見た方が早いので↓↓↓
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()
これは単純に、渡された値が配列かどうかを真偽値で返してくれる。
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じゃない数値だよ〜って判断してしまったりする。
例として以下のような判定がされてしまう………なんてことだ…。
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から新たに導入されたので環境次第では使えない場合も…。
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()
を使うのがベストかな…
まだまだぽっぽこエンジニアなので、間違いなどあればご教示いただけると幸いです。