コメントでの指摘に伴い、公開から間もなく内容を大規模改修しました。ご指摘頂いた @think49 さん、ありがとうございました。
タイトル:
JavaScript でも、Python の type みたいに本当の型を取得したい
-> JavaScript でも、Python の type みたいに詳しく型orオブジェクト名を得る方法を探る
まだまだ知らないことだらけや・・・。
あと、参考にするどころか、結局 9割こちらの記事のパクりになってしまった・・・。
先に最終コードが必要なら、スキップしてこちらの章をどうぞ。
Python の type
では
type(5)
# <class 'int'>
type("hello")
# <class 'str'>
from datetime import timedelta
obj = timedelta()
type(obj)
# <class 'datetime.timedelta'>
type
に任意のオブジェクトを渡すと、しっかりクラス名を出してくれます。
一方 JavaScript では・・・
int
や str
など、プリミティブ型の判断は typeof
によって実現できます。
console.log(typeof 5);
// number
console.log(typeof "hello");
// string
しかし、プリミティブ型以外は、null
も含めて全部 object
と返されてしまうという問題があります。でも実際、これはちゃんと JavaScript 規格通りで、不具合でもなんでもありません。
console.log(typeof null);
// object
console.log(typeof []);
// object
typeof null === "object"
となるのは、歴史的経緯のためのようです。
「肝心なその Object が、もっと詳しく何型か知りたい!」おそらく、そんな経緯でこの記事にたどり着いたのではないかと思っています。
本題に入る前に・・・型の話
JavaScript の世界、どうやら型の概念がメチャややこしいみたいなんです。まとめるだけまとめたけど、正直あってるか不安・・・。
とりあえず要点だけ抜くと、
-
typeof
でわかるヤツだけが、本当の「型」- null は object と出るが、ちゃんと null 型がある
- function は function って出るけど、型じゃなくて Object の一種
- 型のうち Object 以外をプリミティブ型と言う
- 同じプリミティブでも、状況によって「プリミティブラッパーオブジェクト (つまり Object の一種)」に変身することも
- Object のもっと詳しい名前のことは「標準組み込みオブジェクト名」あるいは「クラス名」
ちなみに Python では、(type
の表示によると) int も str も自作クラスも全部 Class となっています。同じ動的型付け言語だけど全然違う!
真の姿を確かめるのに使えそうなもの
ここで、自作クラスとして次を用意して検証しました。
class MyAnyObj {
constructor() {}
}
myAnyObj = new MyAnyObj();
typeof
- JavaScript 規格の定義上、本来の「型」を得られる
- どの値を入れてもエラーにならない
- 得られるもの
- プリミティブ型 (null 除く)
- プリミティブラッパーオブジェクト (Object と出る)
- 標準組み込みオブジェクト名 (Object と出る)
- クラス名 (Object と出る)
typeof undefined; // 'undefined'
typeof 2; // 'number'
typeof new Number(2); // 'object'
typeof ["hello"]; // 'object'
typeof myAnyObj; // 'object'
Object.prototype.toString.call
- どの値を入れてもエラーにならない
- プリミティブはラッパーオブジェクトに化ける
- 得られるもの
- プリミティブ型 (ラッパーに化ける)
- プリミティブラッパーオブジェクト (ただし本当のプリミティブと混ざる)
- 標準組み込みオブジェクト名
- クラス名 ([object Object] と出る)
Object.prototype.toString.call(undefined); // '[object Undefined]'
Object.prototype.toString.call(2); // '[object Number]'
Object.prototype.toString.call(new Number(2)); // '[object Number]'
Object.prototype.toString.call([]); // '[object Array]'
Object.prototype.toString.call(myAnyObj); // '[object Object]'
'[object Object]'.slice(8, -1); // 'ObjClassName'
// String の slice メソッドで不要な箇所をカット
一応 .toString() と String() も試してみたけど、全然ダメそうだった。
.toString
undefined.toString(); // SyntaxError
2.toString(); // '2'
new Number(2).toString(); // '2'
["hello"].toString(); // 'hello'
myAnyObj.toString(); // '[object Object]'
String()
String(undefined); // 'undefined'
String(2); // '2'
String(new Number(2)); // '2'
String("hello"); // 'hello'
String(myAnyObj); // '[object Object]'
<obj>.constructor.name
- コンストラクタ関数を取得し、その名前 (=クラス名) を得るという計算
-
undefined
など一部プリミティブではエラーになる - 一部プリミティブはラッパーオブジェクトに化ける
-
constructor
を書き換えられてしまっていたら、正しい結果にならない- つまりユーザの具合で結果を改変できる
- (でもこれを書き換えてしまうことってあるのか・・・?)
- 得られるもの
- プリミティブ型 (一部エラー, ラッパーに化ける)
- プリミティブラッパーオブジェクト (ただし本当のプリミティブと混ざる)
- 標準組み込みオブジェクト名
- クラス名
undefined.constructor.name; // TypeError
2.constructor.name; // SyntaxError
"hello".constructor.name; // 'String'
// ↑ ラッパーオブジェクトに化けるのとそうでないのとある
[].constructor.name; // 'Array'
myAnyObj.constructor.name; // 'MyAnyObj'
// 2 (Number) は一度変数に入れたら動く, ただしラッパーオブジェクト化
num = 2;
num.constructor.name; // 'Number'
constructor
書き換えの例:
obj = new Array();
obj.constructor = "hello";
obj.constructor; // 'hello'
obj.constructor.name; // undefined
関数にまとめる
正確性重視の判定関数
以上を踏まえて、任意のオブジェクトの判定は、このようなコードに落ち着きました。
function typeof_improved(obj) {
// null だけちょっと例外的なので先に除外
if(obj === null) {
return "null";
}
// プリミティブの判定
let obj_type = typeof obj;
if(obj_type !== "object") {
return obj_type;
}
// 標準組み込みオブジェクトの判定
obj_type = Object.prototype.toString.call(obj).slice(8, -1);
if(obj_type !== "Object") {
return obj_type;
}
// クラス名取得
return obj.constructor.name;
// 最後は constructor が書き換えられていないことを前提にするしかない
// ちなみに - {any: "value"} のような、クラスでもないオブジェクトなら、ここで "Object" となる
}
(ワンライナーにするには厳しいコード量・・・)
簡易版 (ザックリ分かれば良い)
「本当のプリミティブ or プリミティブラッパーオブジェクト・・・は大事じゃない」という条件下で使える、簡易版はこちら。
function typeof_improved(obj) {
let type = Object.prototype.toString.call(obj).slice(8, -1);
if(type !== "Object") {
return type;
} else {
return obj.constructor.name;
}
}
三項演算子で圧縮すると、
let obj_type = Object.prototype.toString.call(obj).slice(8, -1);
obj_type = obj_type !== "object" ? obj_type : obj.constructor.name;
そもそも、「この型か否か」さえ分かれば良いなら
instanceof
でヨシ。
if(obj instanceof Array) {
//
}
※ ただし、prototype
が書き換えられていなければの話
詳しくは、上で示した記事の「instanceof演算子を用いた判定」の章にて (丸投げ)
旧記事内容
正確でなかった、旧内容はこちら
他の方法で型名を得る
null
と undefined
を除いて、.constructor.name
を呼び出すことによっても型名を知ることができます。この方法だと、オブジェクトのクラス名を知ることができる点で優位です。
1. null
か判断する
シンプルに比較演算子で OK。例外的な動作をするのは先に弾いておきます。
obj === null
2. プリミティブ型か判断する
もしプリミティブ型なら、そのまま typeof
の結果を返せば OK。
typeof obj !== "object"
3. クラス名を得る
残るは Object
のみなので、安心してクラス名取得を行えます。
obj.constructor.name
以上をまとめて
コードに起こすとこんな感じ。
function true_obj_name(obj) {
if(obj === null) {
return "null";
}
if(typeof obj !== "object") {
return typeof obj;
} else {
return obj.constructor.name;
}
}
3項演算子で圧縮するとこんな感じ。
function true_obj_name(obj) {
return obj === null ? "null" : typeof obj !== "object" ? typeof obj : obj.constructor.name;
}
ワンライナーで書いてしまうとこんな感じ。
const obj = new AnyObj();
const obj_type = obj === null ? "null" : typeof obj !== "object" ? typeof obj : obj.constructor.name;
別解
プリミティブ型については、undefined
を除いて同じように .constructor.name
を呼び出せます。なので、次も同様の結果が得られます。
function true_obj_name(obj) {
if(obj === null) {
return "null";
} else if(typeof obj === "undefined") {
return "undefined";
} else {
return obj.constructor.name;
}
}
この方法で得られる結果は、前者の方法では integer
となるところが Integer
に、 string
となるところが String
というように、プリミティブ型だと頭文字が大文字になります。
おわり
この typeof
アップグレード版みたいの、標準にしてくれないかなぁ・・・。