JavaScript: 型やクラスが何か判別

(2020.4) ページを分割、参照する仕様を更新。

データ型 (type)

JavaScript は prototype-based なオブジェクト指向プログラミング言語で、メジャなプログラミング言語で通常思い描くようなクラスはない。Javaなどメジャな言語はたいてい、class-based のオブジェクト指向言語。

JavaScript にも「型」(type) はある。しかし、JavaScript の型システムは相当に壊れている。大きく、1) プリミティブ値, 2) オブジェクト, 3) 関数がある.

1) プリミティブ値 primitive value

nullundefined を区別するのが珍しい。でも、単に面倒になっているだけのような。

undefined
未定義 (未初期化) を表す値。なお、変数を宣言せずに使用すると, undefined ではなくエラーになる。

関数から戻り値なしで抜けたときの値は undefined. 最後に実行した値ではない.

JavaScript
  1. var y;
  2. document.write(y); // => undefined
  3. document.write(x); // Error: x is not defined
null
undefined とは区別される。つまり, null === undefined は偽になる。
Boolean 型
Boolean 型は truefalse のみ。真偽値判定は, Boolean 型を含むあらゆるオブジェクトに対して行える。エラーにはならない。
数値 (Number 型)
珍しく, 整数と実数の区別はない. なので, 1 / 3 は 0.3333333333333333 になる。
文字列 (String 型)
単一の文字 (またはバイト) を表す文字型はない。これは優れている。文字列を表す String 型のみ。文字列は immutable object. よくある記事で replace() で置換、とあるが、破壊的に変更するのではない。

文字列リテラルは,ダブルクォーテーションかシングルクォーテーションで囲む。どちらで囲んでも扱いに違いはない。

文字列のなかで, \の直後に改行で、文字列を継続する。

Template Literal (ES2015; ES6 で導入) を使えば, 文字列中に式を埋め込むことができる。

エスケープシーケンスは次のいずれか;

エスケープシーケンス説明
\' '
\" "
\\ \ 自身
\b バックスペース <BS>
\f フォームフィード <FF>
\n 改行文字 (LF) \u000a
\r 復帰文字 (CR) \u000d
\t 文字タブ文字 (HT) \u0009
\v LINE TABULATION <VT>
\xhh 16進数表記。hは2桁固定で,0〜9,a〜f,A〜F
\udddd UTF-16 の Unicode表記。dは4桁固定で,0〜9,a〜f,A〜F. surrogate pair は, UCS-4 にせずに, 分けて表記する。えー?
シンボル (Symbol 型)
ES2015で導入。
大きな整数 (BigInt 型)
(2020.4時点はドラフト) ES2020 で導入予定. Numberとは型が異なるので, 1n === 1 が偽になる。

2) オブジェクト (Object型)

3) 関数 (オブジェクトのうち呼出し可能なもの)

JavaScript では,「関数」もオブジェクトであって、プロパティを持てるのが意外です。例えば name プロパティで、関数の名前を得ることができます。

typeof 演算子

typeof 演算子で、値の型を得ることができます。しかし, null については壊れています。また、オブジェクトは, 関数以外はすべて "object" になってしまいます。

値または型 typeof obj 備考
undefined"undefined"
null"object" "null" ではない! 変更が提案されたこともあったが、互換性維持のため。
Boolean"boolean"
Number"number"
String"string"
Symbol"symbol" ES2015 (ES6) で導入.
callメソッドを持つオブジェクト "function"
その他のオブジェクト "object"

nullundefined 以外のプリミティブ値について、メソッド呼び出しの形でその型のメソッドを呼び出すことができます。内部で自動的にオブジェクトに変換される (auto-boxing) ためです。

ただ、boxing されたオブジェクトに対して typeof は, すべて 'object' を返してしまう。混乱の元なので、auto-boxing 以外に自分で次のような式は書くべきでない;

JavaScript
[RAW]
  1. new Boolean(true)
  2. new Number(1.2)
  3. new String("bar")

クラス名を得る

1) Object.prototype.toString

Object.prototype.toString.call() で, "[object クラス名]" が返る。引数としてプリミティブ値やリテラルを与えてもよい (auto-boxingされる).

字面の "prototype" も "call" も間違いではない。call() の引数をレシーバとして Object.prototype.toString() を呼び出す。

null は "[object Null]" になって、混乱している。また、自作のクラスは, 後述の設定をしないとすべて "[object Object]" になってしまう。基本的には組込みクラス向け。あまり簡単ではない。

JavaScript
[RAW]
  1. Object.prototype.toString.call(null) //=> "[object Null]"

具体的な挙動は、次のようになっている. 厳密には「型」で判定しているわけではないが、だいたい型のとおりになる。

条件 Object.prototype.toString
undefined "Undefined"
null "Null"
IsArray(obj) が真"Array"
文字列 "String".
[[ParameterMap]]内部スロットを持つ "Arguments"
[[Call]]内部スロットを持つ (呼出し可能) "Function"
[[ErrorData]]内部スロットを持つ "Error"
[[BooleanData]]内部スロットを持つ "Boolean"
[[NumberData]]内部スロットを持つ "Number" NaN, Infinity もこれになる。
[[DateValue]]内部スロットを持つ "Date"
[[RegExpMatcher]]内部スロットを持つ "RegExp"
@@toStringTag 値が文字列 @@toStringTag
それ以外 "Object"

@@toStringTag Symbol.toStringTag プロパティに文字列を設定すれば、それが得られる。しかしこれ、自作クラスでは, 自動的には設定されない。

設定ずみの組込みクラス (ES2015/ES6): "Array Iterator" "ArrayBuffer" "DataView" "Generator" "GeneratorFunction" "JSON" "Map" "Map Iterator" "Math" "Module" "Promise" "Set" "Set Iterator" "String Iterator" "Symbol" "WeakMap" "WeakSet"

ES2017 (ES8) で, 次が追加になっている: SharedArrayBuffer, Atomics, AsyncFunction

自作クラスで設定するには、次のようにする。

JavaScript
[RAW]
  1. // コンストラクタ
  2. function MyClass() { }
  3. // Object.prototype.toString.call() 対応
  4. MyClass.prototype[Symbol.toStringTag] = 'MyClass'; // ECMAScript 6拡張
  5. // オブジェクトを生成
  6. var obj = new MyClass();
  7. console.log(Object.prototype.toString.call(obj)) //=> "[object MyClass]"

2) constructor.name

現代は、もっと簡単に, オブジェクト.constructor.name でクラス名が得られる。

null, undefined 以外について、プリミティブ値でも大丈夫。

クラスのインスタンスかの判定

巷の解説は、意外と不正確なものが多い。クラス (関数オブジェクト) と prototypeプロパティは別物、ということが意識できていない。

オブジェクト instanceof クラス」(真偽値) で、オブジェクトクラスまたはそのサブクラスのインスタンスか、が分かる。左辺をプリミティブ値にすると, いつでも false になる。(auto-boxing されない.)

実際の動作は, オブジェクト の [[Prototype]] 内部プロパティ(の連なり)と、クラス (関数オブジェクト) の prototype プロパティとが同一オブジェクトの場合, true になる。

右辺は, callable でなければならない。そうでなければ TypeError になる。

似た方法で,「クラス.prototype.isPrototypeOf(オブジェクト)」で、オブジェクトがクラスまたはそのサブクラスのインスタンスの場合に true が返る。プリミティブ値を与えると, 同様に, きまってfalseになる。

オブジェクトの [[Prototype]] 内部プロパティとの一致を確認するのも同じ。

これら二つは、通常は同じだが, クラスを使わず直接プロトタイプからオブジェクトを生成するようなケースで、挙動が異なる。

JavaScript
[RAW]
  1. // プロトタイプ
  2. const person_proto = {
  3. isHuman: false,
  4. introduce: function() {
  5. return `Name is ${this.name}. ${this.isHuman ? "Human" : "Non-human"}`;
  6. }
  7. };
  8. // クラスと new を使わずに, オブジェクトを生成
  9. const me = Object.create(person_proto);
  10. me.name = "ほりかわ";
  11. me.isHuman = true;
  12. // メソッド呼び出し可能.
  13. console.log(me.introduce()); // "Name is ほりかわ. Human"
  14. // instanceof の右辺は callable でなければならないので、書けない.
  15. //console.log(me instanceof person_proto); // TypeError
  16. // isPrototypeOf のレシーバはプロトタイプでよい.
  17. console.log(person_proto.isPrototypeOf(me)); //=> true

コンストラクタ === クラス

次のようにすれば、もっと簡単に判定できる。上とことなり, スーパークラスとの比較は false になる。これを利用すると, switch文でクラスでの分岐もできる。

JavaScript
[RAW]
  1. function MyClass() { }
  2. var obj = new MyClass();
  3. console.log(obj.constructor === MyClass); // true
  4. console.log(obj instanceof Object); // true. サブクラスのオブジェクトも真
  5. console.log(obj.constructor === Object); // false! 一致したクラスのみ.