LoginSignup
1
0

JavaScript でも、Python の type みたいに詳しく型orオブジェクト名を得る方法を探る

Last updated at Posted at 2023-04-09

コメントでの指摘に伴い、公開から間もなく内容を大規模改修しました。ご指摘頂いた @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 では・・・

intstr など、プリミティブ型の判断は 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 規格の定義上、本来の「型」を得られる
  • :o: どの値を入れてもエラーにならない
  • 得られるもの
    • :o: プリミティブ型 (null 除く)
    • :x: プリミティブラッパーオブジェクト (Object と出る)
    • :x: 標準組み込みオブジェクト名 (Object と出る)
    • :x: クラス名 (Object と出る)
typeof undefined;     // 'undefined'
typeof 2;             // 'number'
typeof new Number(2); // 'object'
typeof ["hello"];     // 'object'
typeof myAnyObj;      // 'object'

Object.prototype.toString.call

  • :o: どの値を入れてもエラーにならない
  • :x: プリミティブはラッパーオブジェクトに化ける
  • 得られるもの
    • :x: プリミティブ型 (ラッパーに化ける)
    • :o: プリミティブラッパーオブジェクト (ただし本当のプリミティブと混ざる)
    • :o: 標準組み込みオブジェクト名
    • :x: クラス名 ([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

  • コンストラクタ関数を取得し、その名前 (=クラス名) を得るという計算
  • :x: undefined など一部プリミティブではエラーになる
  • :x: 一部プリミティブはラッパーオブジェクトに化ける
  • :x: constructor を書き換えられてしまっていたら、正しい結果にならない
    • つまりユーザの具合で結果を改変できる
    • (でもこれを書き換えてしまうことってあるのか・・・?)
  • 得られるもの
    • :x: プリミティブ型 (一部エラー, ラッパーに化ける)
    • :o: プリミティブラッパーオブジェクト (ただし本当のプリミティブと混ざる)
    • :o: 標準組み込みオブジェクト名
    • :o: クラス名
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演算子を用いた判定」の章にて (丸投げ)

旧記事内容

正確でなかった、旧内容はこちら

他の方法で型名を得る

nullundefined を除いて、.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 アップグレード版みたいの、標準にしてくれないかなぁ・・・。

1
0
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
1
0