5
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-21

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

「データ型の判定で、ここでは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)

※1:nullがなぜ'object'判定に…?それはjsの仕様上、仕方のないことだそうで。
※2:関数オブジェクトだけは特別に'function'を返す。

だが、しかし、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));

SymbolBigIntはそもそも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()を使うのがベストかな…

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

5
6
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6