JavaScriptの型にまつわるややこしい仕様について、簡単にまとめてみたいと思います。
5つのプリミティブ型を判定
JavaScriptにおける型とは、数値(number)、文字列(string)、ブール値(boolean)、null、undefinedの5つのプリミティブ型を指します。これらはtypeof演算子を用いて判別する事が出来ます。ただし、nullだけは仕様に反して'object'
を返します。
typeof 'str'; // 'string'
typeof 1; // 'number'
typeof true; // 'boolean'
typeof null; // 'object' (ECMAScript標準規格によれば独自の型のはずだが...)
typeof undefined; // 'undefined'
その他は全てオブジェクトです。ただし、関数オブジェクトだけは特別に'function'
を返します。
typeof {}; // 'object'
typeof []; // 'object'
typeof function() {}; // 'function' 関数オブジェクトは特別扱い
オブジェクトの[[Class]]を用いた判定
プリミティブ型は判別できましたが、これでは様々なオブジェクト(Object、Array等)の判別は出来ません。しかし、「型」の判定をしたいと言う場合、実際にはArrayやObjectの区別をしたい場合が多いかと思います。
そこで、Object.prototype.toString
を用いて、オブジェクトの[[Class]]
内部プロパティを取得する方法がしばしばとられます。出力は、[object [[Class]]]
という形式になります。
参考: Qiitaページ
var toString = Object.prototype.toString
toString.call({}); // [object Object]
toString.call([]); // [object Array]
toString.call(function() {}); // [object Function]
toString.call(new Error()); // [object Error]
toString.call(new Date()); // [object Date]
toString.call(JSON); // [object JSON]
toString.call(Math); // [object Math]
toString.call(new RegExp()); // [object RegExp]
toString.call(new String('str')); // [object String]
toString.call(new Number(1)); // [object Number]
toString.call(new Boolean(true)); // [object Boolean]
最後の3つは、それぞれ文字列(string)、数値(number)、ブール値(boolean)のラッパーオブジェクトになります。ラッパーオブジェクトというのはstring, number, booleanの3つのプリミティブな値に対してメソッドを呼び出した際に、メソッド実行用に一時的に生成されるオブジェクトです。その為、コンストラクタを使わずにtoString
を呼び出す事が出来ます。
toString.call('str'); // [object String]
toString.call(1); // [object Number]
toString.call(true); // [object Boolean]
また、ECMAScript5からは、null
やundefined
も[[Class]]
を返すようになりました。
参考: MDN公式ドキュメント toString
toString.call(null); // [object Null]
toString.call(undefined); // [object Undefined]
これで、標準コンストラクタで生成されたオブジェクトは判別出来る事が分かりました。
constructorプロパティを用いた判定
ただし、問題はまだあります。自分で実装したコンストラクタから生成したオブジェクトは、Object.prototype.toString
を使っても[object Object]
にしかなりません。
function MyClass() {}
var obj = new MyClass();
Object.prototype.toString.call(obj); // [object Object]
これを判別する一つの方法として、constructorプロパティを用いるといった方法が考えられます。
obj.constructor === MyClass; // true
ただし、文字列としては取得出来ず、さらにconstructor
プロパティが上書きされる危険性もあります。また、コンストラクタのprototype
プロパティを書き換えていると、おかしな事になる場合もあります。
obj.constructor === 'MyClass'; // false
obj.constructor = Array; // constructorプロパティを上書き
obj.constructor === MyClass; // false
function MyClass2() {} // 新しいコンストラクタを定義
MyClass2.prototype = {}; // コンストラクタのprototypeを書き換え
var obj2 = new MyClass2();
obj2.constructor === MyClass2; // false
obj2.constructor === Object; // true
確実な判定方法とは言えないかもしれません。
instanceof演算子を用いた判定
instanceof演算子は、オブジェクトがどのコンストラクタから生成されたかを判別する為に使われます。オブジェクトの「広義の型」の判定に使えそうです。
function MyClass() {}
var obj = new MyClass();
obj instanceof MyClass; // true
obj instanceof Array; // false
ここで、instanceof演算子の挙動について考えてみます。instanceof演算子は、内部的にはオブジェクト自身のプロトタイプチェーンにコンストラクタのprototype
が含まれているかをチェックしています。
参考: MDN公式ドキュメント instanceof
obj instanceof MyClass; // true
obj.__proto__ === MyClass.prototype; // true
obj instanceof Object; // true
obj.__proto__.__proto__ === Object.prototype; // true
その為、オブジェクトのプロトタイプが書き換えられた場合にはinstanceof
の返り値も変わってしまいます。
obj.__proto__ = Array.prototype;
obj instanceof MyClass; // false
obj instanceof Array; // true
よって、これも確実な判定方法とはならないでしょう。
結論
標準コンストラクタから生成されたオブジェクトの判定には、Object.prototype.toString
を使うと良いでしょう。自前のコンストラクタから生成されたオブジェクトの判定にはいくつか方法がありますが、上であげた2つはどちらも確実な方法とはいえません。__proto__プロパティの書き換えは決してしない等の規約を決めた上で、判定を行うと良いでしょう。