2021年12月12日

クラスチェック:「instanceof」

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

このようなチェックは多くの場合に必要になる場合があります。たとえば、引数をその型に応じて異なる方法で処理する、ポリモーフィックな関数の構築に使用できます。

instanceof演算子

構文は次のとおりです。

obj instanceof Class

objClassまたはそこから継承するクラスに属する場合はtrueを返します。

たとえば

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

// is it an object of Rabbit class?
alert( rabbit instanceof Rabbit ); // true

コンストラクタ関数でも機能します

// instead of 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の動作をカスタマイズできます。

    たとえば

    // setup instanceOf check that assumes that
    // anything with canEat property is an animal
    class Animal {
      static [Symbol.hasInstance](obj) {
        if (obj.canEat) return true;
      }
    }
    
    let obj = { canEat: true };
    
    alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj) is called
  2. ほとんどのクラスにはSymbol.hasInstanceがありません。その場合、標準のロジックが使用されます。obj instanceOf Classは、Class.prototypeobjのプロトタイプチェーンのプロトタイプの1つと等しいかどうかをチェックします。

    言い換えれば、次々に比較します。

    obj.__proto__ === Class.prototype?
    obj.__proto__.__proto__ === Class.prototype?
    obj.__proto__.__proto__.__proto__ === Class.prototype?
    ...
    // if any answer is true, return true
    // otherwise, if we reached the end of the chain, return false

    上記の例ではrabbit.__proto__ === Rabbit.prototypeなので、すぐに答えが得られます。

    継承の場合、2番目のステップで一致が見つかります。

    class Animal {}
    class Rabbit extends Animal {}
    
    let rabbit = new Rabbit();
    alert(rabbit instanceof Animal); // true
    
    // rabbit.__proto__ === Animal.prototype (no match)
    // 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();

// changed the prototype
Rabbit.prototype = {};

// ...not a rabbit any more!
alert( rabbit instanceof Rabbit ); // false

ボーナス:型のObject.prototype.toString

プレーンオブジェクトは[object Object]として文字列に変換されることを既に知っています。

let obj = {};

alert(obj); // [object Object]
alert(obj.toString()); // the same

これはそれらのtoStringの実装です。しかし、toStringを実際にはそれよりもはるかに強力にする隠れた機能があります。これを拡張typeofinstanceofの代替として使用できます。

奇妙に聞こえますか?確かに。解き明かしましょう。

仕様によると、組み込みのtoStringはオブジェクトから抽出して他の値のコンテキストで実行できます。その結果は、その値によって異なります。

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

実演しましょう。

// copy toString method into a variable for convenience
let objectToString = Object.prototype.toString;

// what type is this?
let arr = [];

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

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

内部的に、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 for the environment-specific object and class:
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の代わりに{}.toString.callを使用できます。

要約

私たちが知っている型チェックの方法を要約しましょう。

有効な対象 戻り値
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は関数ではなく、プロトタイプチェーンに対して一致させるプロトタイプを気にします。

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

したがって、instanceofのロジックによれば、プロトタイプは実際にはコンストラクタ関数ではなく、型を定義します。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点の提案がある場合は、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • コードを数単語挿入するには<code>タグを使用し、数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。