コンストラクタを設定し直すのは必要か?
この記事では「プロトタイプでオブジェクトの継承を実装する」で説明した、 オブジェクトの継承方法について、少し細かいポイントについて説明します。
はじめて JavaScript を学習される方は、 とりあえず「プロトタイプでオブジェクトの継承を実装する」で説明した方法に従うので問題ないはずです。
さて、以前の記事「プロトタイプでオブジェクトの継承を実装する」では Personクラスを継承して、Employeeクラスを作成するにあたり、 次のように実装すると説明しました。
//コンストラクタ
function Employee(name, age, title) {
Person.call(this, name, age);
this.title = title;
}
//プロトタイプ
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
//メソッド
Employee.prototype.bar = function() {
console.log(`bar: ${this.name}(${this.age}) ${this.title}`);
};
Employeeの中では、call() を用いて、 基底クラスとなる Personのコンストラクタを直接呼び出しています。
これはnewも付けていないですし、コンストラクタに thisを渡しつつ、 単純に呼び出しているだけです。
一方、次の行では、お行儀よくプロトタイプチェーンの中のコンストラクタを設定し直しています。
Employee.prototype.constructor = Employee;
もし継承する際に、親クラスのコンストラクタを直接呼び出すのであれば、わざわざプロトタイプチェーンの中のコンストラクタを整備しておく必要はあるのでしょうか?
言い換えると、プロトタイプチェーンの中のコンストラクタなんて、使われることあるの?ということになります。
ここでは、この疑問について考えてみましょう。
はじめに答えを書くと、コンストラクタを呼ぶことがあれば呼ばれます、ということです。
呼べば呼ばれる、って当たり前のことのようですが、ポイントはconstructorが明示的に呼ばれることがあれば困ることが起きる場合があるということです。
この問題は具体例で考えるとわかりやすいです。
例えば、基底クラスでオブジェクトのコピーを行うcopyメソッドを実装するとします。
正しく実装すれば、次のようになります。
//基底クラス
function Base() {
console.log('+ Base constructor called');
this.name = "base";
}
// 基底クラスで copy メソッドを実装
Base.prototype.copy = function() {
console.log('copy called');
return new this.constructor();
}
//派生クラス
function Derived() {
console.log('+ Derived constructor called');
Base.call(this);
this.name = "derived";
}
Derived.prototype = Object.create(Base.prototype);
Derived.prototype.constructor = Derived;
//インスタンスの作成
let o = new Derived();
let c = o.copy(); // コピーを作成
//オブジェクトのチェック
console.log('Object - o');
console.log(' Base? ' + (o instanceof Base));
console.log(' Derived? ' + (o instanceof Derived));
console.log('Object - c');
console.log(' Base? ' + (c instanceof Base));
console.log(' Derived? ' + (c instanceof Derived));
実行結果は次のようになります。
copyメソッドを呼び出して作成したオブジェクトcは、 想定通りDeviedのオブジェクトであることが確認できました。
copyメソッドから、Derivedコンストラクタが呼ばれていることも確認できます。
そこで、もしコンストラクタの設定し直しを行わない場合はどうなるでしょうか。
//...
Derived.prototype = Object.create(Base.prototype);
//Derived.prototype.constructor = Derived; // コメントアウト
//...
この実行結果は次の通りになります。
コピーされたオブジェクトは、Derived オブジェクトではなく、 Base オブジェクトとなります。
上の正常動作する場合 (コンストラクタを再設定した場合) には、プロトタイプチェーンを上から辿れば、 constructor メソッドとして Derivedが設定されています。
このため、たとえ基底クラスで定義されたメソッドからでも、thisからconstrucotrを呼び出した時には、 派生クラスのconstructorが呼ばれます。
しかし、constructorを設定していない場合は、次のようにプロトタイプチェーンの中に派生クラスのconstructorがありません。Baseのみです。
このため基底クラスのコンストラクタによって、オブジェクトが作成されることになり、結果として基底クラスのオブジェクトとしてしか認識されないことになります。
以上の理由で、コンストラクタのprototypeプロパティを上書いた後には、 コンストラクタを付け直しておくと良いということになります。