2022年4月27日

クラスのチェック: "instanceof"

instanceof 演算子でオブジェクトが特定のクラスに属しているのかを確認することができます。また、継承も考慮されます。

このようなチェックが必要なケースは多々あるかもしれません。ここでは、その型に応じて引数を別々に扱う 多形(ポリモーフィック) 関数を構築するために使用します。

instanceof 演算子

構文は次の通りです:

obj instanceof Class

それは objClass (または、それを継承しているクラス)に属している場合に true を返します。

例:

class Rabbit {}
let rabbit = new Rabbit();

// Rabbit クラスのオブジェクト?
alert( rabbit instanceof Rabbit ); // true

コンストラクタ関数でも動作します。:

// class の代わり
function Rabbit() {}

alert( new Rabbit() instanceof Rabbit ); // true

…また Array のような組み込みクラスでも動作します。:

let arr = [1, 2, 3];
alert( arr instanceof Array ); // true
alert( arr instanceof Object ); // true

arrObject クラスにも属していることに留意してください。Array はプロトタイプ的に Object を継承しているためです。

通常、instanceof 演算子はチェックのためにプロトタイプチェーンを検査します。この動きに対して、静的メソッド Symbol.hasInstance でカスタムロジックが設定できます。

obj instanceof Class のアルゴリズムはおおまかに次のように動作します。:

  1. もし静的メソッド Symbol.hasInstance があれば、それ(Class[Symbol.hasInstance](obj))を使います。これは true または false を返す必要があり、これで instanceof の振る舞いがカスタマイズできます。:

    例:

    // catEat プロパティをもつものは animal と想定する
    // instanceOf チェックを設定
    class Animal {
      static [Symbol.hasInstance](obj) {
        if (obj.canEat) return true;
      }
    }
    
    let obj = { canEat: true };
    
    alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj)が呼ばれます
  2. ほとんどのクラスは Symbol.hasInstance を持っていません。このケースでは、通常のロジックが使用されます: obj instanceOf ClassClass.prototypeobj のプロトタイプチェーンうちの1つと等しいかをチェックします。

    言い換えると、以下のような比較を行います:

    obj.__proto__ == Class.prototype
    obj.__proto__.__proto__ == Class.prototype
    obj.__proto__.__proto__.__proto__ == Class.prototype
    ...
    // いずれかが true の場合、 true が返却されます
    // そうでない場合、チェーンの末尾に到達すると false を返します

    上の例では、``rabbit.proto === Rabbit.prototype` なので、すぐに回答が得られます。

    継承のケースでは、2つめのステップでマッチします:

    class Animal {}
    class Rabbit extends Animal {}
    
    let rabbit = new Rabbit();
    alert(rabbit instanceof Animal); // true
    
    // rabbit.__proto__ == Rabbit.prototype
    // rabbit.__proto__.__proto__ === Animal.prototype (match!)

これは、rabbit instanceof AnimalAnimal.prototype を比較したものです。:

ところで、objA.isPrototypeOf(objB) というメソッドもあります。これは objAobjB のプロトタイプチェーンのどこかにあれば true を返します。なので、obj instanceof Class のテストは Class.prototype.isPrototypeOf(obj) と言い換えることができます。

面白いことに、Class コンストラクタ自身はチェックには参加しません! プロトタイプと Class.prototypeのチェーンだけです。

これは prototype が変更されたときに興味深い結果につながります。

このように:

function Rabbit() {}
let rabbit = new Rabbit();

// prototype を変更します
Rabbit.prototype = {};

// ...もう rabbit ではありません
alert( rabbit instanceof Rabbit ); // false

おまけ: 型のための Object toString

私たちは通常の文字列は [object Object] という文字列に変換されることをすでに知っています。:

let obj = {};

alert(obj); // [object Object]
alert(obj.toString()); // 同じ

これが toString の実装です。しかし、実際にはそれよりもはるかに強力な toString を作る隠れた機能があります。それを拡張させて typeof または instanceof の代替として利用することができます。

奇妙に聞こえますか?たしかに。分かりやすく説明しましょう。

スペック(specification)によって、組み込みの toString はオブジェクトから抽出し、任意の値のコンテキストで実行することができます。そして、その結果はその値に依存します。

  • 数値の場合、それは [object Number] になります。
  • 真偽値の場合、[object Boolean] になります。
  • null の場合: [object Null]
  • undefined の場合: [object Undefined]
  • 配列の場合: [object Array]
  • …など (カスタマイズ可能).

デモを見てみましょう:

// 使いやすくするために toString メソッドを変数にコピー
let objectToString = Object.prototype.toString;

// これの型はなに?
let arr = [];

alert( objectToString.call(arr) ); // [object Array]

ここでは、コンテキスト this=arr で関数 objectToString を実行するため、デコレータと転送, call/apply の章で説明した call を使いました。

内部的には、toString アルゴリズムは this を検査し、対応する結果を返します。ほかの例です。:

let s = Object.prototype.toString;

alert( s.call(123) ); // [object Number]
alert( s.call(null) ); // [object Null]
alert( s.call(alert) ); // [object Function]

Symbol.toStringTag

Object toString の振る舞いは特別なオブジェクトプロパティ Symbol.toStringTag を使用してカスタマイズできます。

例:

let user = {
  [Symbol.toStringTag]: 'User'
};

alert( {}.toString.call(user) ); // [object User]

ほとんどの環境固有のオブジェクトには、このようなプロパティがあります。これはいくつかのブラウザ固有の例です。:

// 環境固有のオブジェクトとクラスのtoStringTag:
alert( window[Symbol.toStringTag]); // window
alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest

alert( {}.toString.call(window) ); // [object Window]
alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest]

ご覧の通り、結果は正確に Symbol.toStringTag (存在する場合)で、[object ...] の中にラップされています。

最終的には、プリミティブなデータ型だけでなく、組み込みオブジェクトのためにも機能し、カスタマイズすることもできる “強化された typeof” があります。

これは、型を文字列として取得するだけでなく、チェックするために、組み込みオブジェクトに対して instanceof の代わりに使用できます。

サマリ

私たちが知っている型チェックメソッドについて再確認しましょう:

対象 戻り値
typeof プリミティブ 文字列
{}.toString プリミティブ, 組み込みオブジェクト, Symbol.toStringTag をもつオブジェクト 文字列
instanceof オブジェクト true/false

ご覧のように、{}.toString は技術的には “より高度な” typeof です。

そして、instanceof 演算子は、クラス階層を扱っていて継承を考慮したクラスのチェックをしたい場合に本当に輝きます。

タスク

重要性: 5

なぜ下の instanceoftrue を返すのでしょう? aB() によって作られたものでないことは簡単に分かります。

function A() {}
function B() {}

A.prototype = B.prototype = {};

let a = new A();

alert( a instanceof B ); // true

はい、確かに奇妙に見えます。

しかし、instanceof は関数を気にするのではなく、プロトタイプチェーンに対してマッチする prototype について気にします。

そして、ここでは a.__proto__ == B.prototype なので、instanceoftrue を返します。

従って、instanceof のロジックに基づいて、prototype は実際にはコンストラクタ関数ではなく型を定義します。

チュートリアルマップ