JavaScript の instanceof は何をチェックしているのか?

特定のクラスのオブジェクトかどうか調べる

instanceof でオブジェクトがあるクラスのインスタンスであるか確認できる

この記事では内部動作についての説明を行います。JavaScript を初めて学ぶひとは、instanceof オペレータでオブジェクトの種類がわかる、ということをわかっていれば十分です。

プロトタイプでオブジェクトの継承を実装する」では、 JavaScript ではプロトタイプチェーンを設定することで、クラスの継承関係を定義することを説明しました。

ES6 以降はclassキーワードとextendsを用いてクラスの継承を行いますが、 出来上がったオブジェクトが同様のプロトタイプチェーンを持つことには違いはありません。

例えば、クラスAからBを派生して、 さらにCを派生する場合、次のように書けます。

function A() {}

function B() {}

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

function C() {}

C.prototype = Object.create(B.prototype);
C.prototype.constructor = C;

ここでCのオブジェクトを作ります。

let c = new C();

そして、instanceofオペレータを用いて、 そのオブジェクトがCのオブジェクトであるか、 Bのオブジェクトであるか、 Aのオブジェクトであるかチェックしてみます。

let c = new C();

console.log('Is c an instance of C? ' + (c instanceof C));
console.log('Is c an instance of B? ' + (c instanceof B));
console.log('Is c an instance of A? ' + (c instanceof A));

実行結果は次の通りです。

これによって、ちゃんと継承関係が有効に設定されていることがわかります。

プロトタイプチェーン内に継承関係がみえるのか?

cオブジェクトをダンプしてプロトタイプチェーンをみると、 次のようにキレイにチェーンが確認できるので、何の問題もないようにみえます。

さて、ではもう少し実験してみましょう。

コンストラクタを設定し直すのは必要か?」では、 「prototypeプロパティを設定した後、コンストラクタを付け戻すのは必ずしも必要ではないけど、しておかないと困ることが起きることがあるので、しておいた方が良いでしょう」というようなことを例を混えて説明しました。

つまりコンストラクタの設定は必ずしも必要ではありません。

そこで、次のようにしてもう一度実験します。今度はプロトタイプを書き換えた後、コンストラクタを付け戻していません。

function A() {}

function B() {}
B.prototype = Object.create(A.prototype);

function C() {}
C.prototype = Object.create(B.prototype);

let c = new C();

console.log('Is c an instance of C? ' + (c instanceof C));
console.log('Is c an instance of B? ' + (c instanceof B));
console.log('Is c an instance of A? ' + (c instanceof A));

実はこれでも結果は変わりません。instanceof を使えば相変わらず、 オブジェクト c は同時に、 ABC 全てのオブジェクトであるということを把握できます。

このとき、プロトタイプチェーンは次のようにみえます。

BCという文字は表示されていません。

上は Chrome のデバッグツールでの表示結果でした。もしかしたら、デバッグツールがうまく表示できていないだけかもしれないので、Firefox でみてみましょう。

BCの文字はみえません。

Safari を使っても同様です。

BCというのは全く見えてこないのですが、instanceof はなぜ、 それらのオブジェクトであることを区別できるのでしょうか?

ダミーのコンストラクタをセットしたらどうなるか?

さらに実験として、次のようにダミーのコンストラクタ Dummy を用意して、 プロトタイプチェーン内のコンストラクタに適当にセットしてみます。

function Dummy() {
}

function A() {}

function B() {}
B.prototype = Object.create(A.prototype);
B.prototype.constructor = Dummy;

function C() {}
C.prototype = Object.create(B.prototype);
C.prototype.constructor = Dummy;

let c = new C();

console.log('Is c an instance of C? ' + (c instanceof C));
console.log('Is c an instance of B? ' + (c instanceof B));
console.log('Is c an instance of A? ' + (c instanceof A));
console.log('Is c an instance of Dummy? ' + (c instanceof Dummy));

console.log(c);

この結果も次のように、正しく継承関係が認識されています。

instanceofオペレータが、コンストラクタの関係をチェックしているわけではないことは、この結果からもわかります。

では、instanceofは何をチェックしているのでしょうか。

instanceof@@hasInstanceを利用する

実は JavaScript には、プロトタイプチェーンが設定されたときに、そのチェーンの情報をトラッキングする仕組みがあります。

instanceofは、内部的に保持している@@hasInstanceというメソッドを呼びます。 このメソッドはSymbol.hasInstanceというウェルノウンシンボル (well-known symbol) で参照できます。

@@hasIntance はプロトタイプチェーンを確認して、クラスの継承関係 (親子関係) を識別できるように実装されています。

このため、コンストラクタの設定をしようがしまいが、あるいは偽物のコンストラクタが設定されていようが、instanceofは正しい結果を返すことができるのです。

試しに instanceof の代わりに、型[Symbol.hasInstance](オブジェクト) として動作を確認します。

let c = new C();

console.log('Is c an instance of C? ' + (C[Symbol.hasInstance](c)));
console.log('Is c an instance of B? ' + (B[Symbol.hasInstance](c)));
console.log('Is c an instance of A? ' + (A[Symbol.hasInstance](c)));
console.log('Is c an instance of Dummy? ' + (Dummy[Symbol.hasInstance](c)));

この結果は次のように、instanceofと同様の結果を返します。

以上、ここではinstanceofが内部で利用する@@hasInstanceメソッドについて説明しました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 JavaScript 入門