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 は同時に、 A、B、C 全てのオブジェクトであるということを把握できます。
このとき、プロトタイプチェーンは次のようにみえます。
BやCという文字は表示されていません。
上は Chrome のデバッグツールでの表示結果でした。もしかしたら、デバッグツールがうまく表示できていないだけかもしれないので、Firefox でみてみましょう。
BやCの文字はみえません。
Safari を使っても同様です。
BやCというのは全く見えてこないのですが、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メソッドについて説明しました。