JavaScriptプロトタイプチェーン(4)クラスの継承(Classical Inheritance)を図示する

ラベル:

前の関連記事:JavaScriptプロトタイプチェーン(3)すべてのプロトタイプチェーンはObject.prototypeオブジェクトで終わる


プロトタイプベースのオブジェクト指向言語であるJavaScriptでクラスベースのオブジェクト指向言語を模倣するときは関数オブジェクトを作成したときに暗黙的に生成されるプロトタイプオブジェクトを「クラス」と模倣します。

「Show all functions」ですべての関数オブジェクトを図示する

var parent = {
    get: function fn() {
         return this.val;
    },
    val: 42
}
var child = Object.create(parent);
child.val = 3.14159
this.parent = parent;
Object Playgroundでこのコードを「Show all functions」のチェックのみをつけて「Click to Evaluate」をクリックします。

parentオブジェトのgetプロパティの関数オブジェクトfn()のノードがグラフに表示されました。

さらにそのfn()のprototypeプロパティにはfn.prototypeオブジェクトが入っています。

fn.prototypeオブジェクトにはconstructorプロパティがありそこにはfn()オブジェクトが入っています。

ということで関数オブジェクトfn()ノードとfn.prototypeオブジェクトのノードは相互に参照するようにエッジでつながっています。

関数を定義すると関数オブジェクトに加えて暗黙的にプロトタイプオブジェトが生成されている

function fn() {};
this.fn=fn;
オブジェクトビジュアライザーで「Show all functions」をチェックしてこのコードからグラフを作成すると関数オブジェクトfn()のノードのprototypeプロパティからfn.prototypeオブジェクトのノードへエッジが伸びていることがわかります。

このfn.prototypeオブジェクトは関数fn()を定義したときに暗黙的に生成されるオブジェクトです。

fn.prototypeオブジェクトのconstructorプロパティにはこのfn.prototypeオブジェクトを作った関数オブジェクトが入ることにより、関数オブジェクトとプロトタイプオブジェクトは相互に参照する形式になっています。

関数定義時に暗黙的に生成されるプロトタイプオブジェクトをクラスに模倣する


JavaScriptでクラスを模倣するときはこの暗黙的に生成されるプロトタイプオブジェクトを「クラス」と模倣します。

WSH JScriptでJavaScriptのお勉強(関数定義、クロージャ、this)JavaScriptプロトタイプチェーン(1)プロトタイプチェーン図示ツールで使ったthis - JavaScript | MDNの例で関数のプロトタイプオブジェクトをクラスと模倣する状態をオブジェクトビジュアライザーでみてみます。

まずはコンストラクタ関数Car()を作ります。
function Car(brand) {
    this.brand = brand;
}
this.Car=Car;
nameプロパティに関数名Car、prototypeプロパティにCar.prototype、lengthプロパティに引数の数1が入ったコンストラクタ関数Car()のノードがグラフに描かれ、さらにそのノードのprototypeプロパティからCar.prototypeオブジェクトへエッジが描かれ、逆にCar.prototypeのconstructorプロパティからCar()関数オブジェクトへのエッジが描かれています。

暗黙的に生成されたCar.prototypeオブジェクトを「クラス」として模倣するので、疑似クラスCarにメソッドを追加するときはCarオブジェクトではなくてCar.prototypeオブジェクトの方に追加します。
function Car(brand) {
    this.brand = brand;
}
Car.prototype.getBrand = function () {
    return this.brand;
}
this.Car=Car;
4行目でCar.prototypeオブジェクト(Carのprototypeプロパティではなく!)にgetBrandプロパティを作って、そこに関数(メソッド)を定義しています。

「メソッド」といってもクラスモデルを模倣しているのでそう呼んでいるだけで、JavaScriptとしてはCar.prototypeオブジェクトのプロパティに関数オブジェクトが割り当てられただけです。

オブジェクトビジュアライザーのグラフを見るとCar.prototypeオブジェクトにgetBrandプロパティが追加され、その値に無名関数が入っていることがわかります。

暗黙的なCar.prototypeオブジェクトに明示的にプロパティを追加したので「Show all functions」をチェックしていなくてもCar.prototypeオブジェクトのノードが描画されるようになります。

今度はこのCar「クラス」をnew演算子でインスタンス化します。
function Car(brand) {
    this.brand = brand;
}
Car.prototype.getBrand = function () {
    return this.brand;
}
var foo = new Car("toyota");
this.foo=foo; 
コンストラクタ関数Car()をクラスと見立ててnew演算子でインスタンス化したインスタンスfooはbrandというプロパティに引数"toyota"を持っています。

このbrandプロパティはインスタンス変数、メンバ変数、フィールド(クラス変数ではない)、属性と呼ばれるものに該当します。(言語によって用語の使われ方が異なるのでややこしい。)

fooインスタンスとコンストラクタ関数Carのオブジェクトは内部プロパティ[[Prototype]]で結ばれるプロトタイプチェーンでは結ばれていないことがグラフからわかります。

ですので例えばfoo.nameとしてもCar()のプロパティまではさがしにいってくれないのでundefinedが返ってきます。

コンストラクタ関数である関数オブジェクトはその名の通りインスタンス化するときの初期化(コンストラクタ)に使われています。

Car.prototypeのconstructorプロパティにはその初期化する関数オブジェクトが入っていますが、Object Playgroundの解説ビデオの20:20頃ではこのプロパティはなくても動作に支障がない、と言っています。

new演算子はCar.prototypeオブジェクトに対してではなく、コンストラクタ関数Car()に対して実施するのでCar.prototypeオブジェクトがコンストラクタを知っている必要はなく、Car()がCar.prototypeをしているだけでよい訳です。

クラスの継承(Classical Inheritance)を行う


まず疑似クラスCarを継承するコンストラクタ関数Model()を作成します。
function Model(brand, drive) {
    this.brand = brand;
    this.drive = drive;
}
this.Model=Model; 
まだModel.prototypeは暗黙的な存在なので「Show all functions」をチェックしておかないとグラフに表示されません。

Model()は引数が2個あるのでlengthプロパティが2になっています。

疑似クラスでは関数オブジェクトのプロトタイプオブジェクトをクラスと模倣するので、疑似クラスCarを継承させるのはModel()オブジェクトではなくてModel.prototypeオブジェクトになる、、、のではなく、Model.prototypeオブジェクトは捨てて関数オブジェクトModel()のprototypeプロパティに、継承したいコンストラクタ関数のプロトタイプオブジェクトにプロトタイプチェーンをもつオブジェクトが入るようにします。
function Car(brand) {
    this.brand = brand;
}
Car.prototype.getBrand = function () {
    return this.brand;
}

function Model(brand, drive) {
    this.brand = brand;
    this.drive = drive;
}
Model.prototype = new Car;
this.Model=Model;
12行目でコンストラクタ関数Model()のprototypeプロパティにコンストラクタ関数Car()をnewでインスタンス化したオブジェクトを入れています。
これでコンストラクタ関数Model()のprototypeプロパティにCar.prototypeへプロトタイプチェーンをもつ、つまり内部プロパティ[[Prototype]]の値がCar.prototypeのオブジェクトModel.prototypeが入りました。(グラフのノードのラベルはModel().prototypeになっています。)

もともとあったModel()のプロトタイプオブジェクトModel.prototypeは参照されることがなくなったのでガベッジコレクタで破棄されます。

先ほど書いたようにconstructorプロパティは利用されていないので設定する必要はないのですが、関数オブジェクトのプロトタイプオブジェクトと同様にグラフで見えるようにconstructorプロパティを設定します。
function Car(brand) {
    this.brand = brand;
}
Car.prototype.getBrand = function () {
    return this.brand;
}

function Model(brand, drive) {
    this.brand = brand;
    this.drive = drive;
}
Model.prototype = new Car;
Model.prototype.constructor = Model;
this.Model=Model;
constructorプロパティを設定することによって関数オブジェクトとそのプロトタイプオブジェクトの相互参照関係が再現できました。

オブジェクトビジュアライザーのノードのラベルもちゃんとModel.prototypeになっています。

こんどはこのModelをインスタンス化します。
function Car(brand) {
    this.brand = brand;
}
Car.prototype.getBrand = function () {
    return this.brand;
}

function Model(brand, drive) {
    this.brand = brand;
    this.drive = drive;
}
Model.prototype = new Car;
Model.prototype.constructor = Model;
this.foo = new Model("subaru", "4WD");
プロトタイプチェーンはfoo→Model.prototype→Car.prototype、になっています。

コンストラクタ関数Modle()のインスタンスはCar.prototypeのプロパティを継承していることがよくわかります。

12行目のModel.prototype = new Car でクラスに模倣しているプロトタイプオブジェクトにインスタンスを入れているのでインスタンス変数brandがプロトタイプオブジェクトのプロパティに意図せず入っています。

このインスタンス変数の紛れ込みはES5から導入されたObject.create()をnew演算子の代わりに使うと解決します。
function Car(brand) {
    this.brand = brand;
}
Car.prototype.getBrand = function () {
    return this.brand;
}

function Model(brand, drive) {
    this.brand = brand;
    this.drive = drive;
}
Model.prototype = Object.create(Car.prototype);
Model.prototype.constructor = Model;
this.foo = new Model("subaru", "4WD"); 
Model.prototypeオブジェクトのノードからbrandプロパティがなくなりましたね。

Object Playgroundの解説ビデオでもClassical InheritanceではObject.create()を使用しています。

instanceof演算子でオブジェクトと、コンストラクタ関数のプロトタイプオブジェクトの関係を確認する


instanceof - JavaScript | MDN

instanceof演算子を使うとオブジェクトがコンストラクタ関数のプロトタイプオブジェクトと同一のプロトタイプチェーン上にあるかを確認できます。

オブジェクト instanceof コンストラクタ関数

オブジェクトがコンストラクタ関数のプロトタイプオブジェクトと同一のプロトタイプチェーン上にあるとtrue、そうでない場合はfalseになります。
//コンストラクタ関数Car()
function Car(brand) {
    this.brand = brand;
}
Car.prototype.getBrand = function () {
    return this.brand;
}
//コンストラクタ関数Model()
function Model(brand, drive) {
    this.brand = brand;
    this.drive = drive;
}
//Model.prototypeをCar.prototypeを[[Prototype]]とするオブジェクトに置換
Model.prototype = Object.create(Car.prototype);
Model.prototype.constructor = Model;
//Model()のインスタンス化
var ins_Model = new Model("subaru", "4WD");
//Car()のインスタンス化
var ins_Car=new Car("toyota");
//オブジェクトビジュアライザーのthisのプロパティを設定
this.ins_Model=ins_Model;
this.ins_Car=ins_Car;

this.ins_Model_Model=ins_Model instanceof Model;
this.ins_Model_Car=ins_Model instanceof Car;
this.ins_Car_Model=ins_Car instanceof Model;
this.ins_Car_Car=ins_Car instanceof Car;
ins_Model instanceof Model

ins_Car instanceof Car

これらは当然trueになります。

ins_Model instanceof Car

ins_Modelからプロトタイプチェーンをたどるとins_Model→Model.prototype→Car.prototype、となるのでこれもtrueになります。

ins_Car instanceof Model

これはins_CarとModel.prototypeをつなぐプロトタイプチェーンがないのでfalseになります。

今度はins_ModelをModel()からインスタンス化したあとにModel.prototypeを置換してみます。
//コンストラクタ関数Car()
function Car(brand) {
    this.brand = brand;
}
Car.prototype.getBrand = function () {
    return this.brand;
}
//コンストラクタ関数Model()
function Model(brand, drive) {
    this.brand = brand;
    this.drive = drive;
}
//Model()のインスタンス化
var ins_Model = new Model("subaru", "4WD");
//Model.prototypeをCar.prototypeを[[Prototype]]とするオブジェクトに置換
Model.prototype = Object.create(Car.prototype);
Model.prototype.constructor = Model;
//Car()のインスタンス化
var ins_Car=new Car("toyota");
//オブジェクトビジュアライザーのthisのプロパティを設定
this.ins_Model=ins_Model;
this.ins_Car=ins_Car;

this.ins_Model_Model=ins_Model instanceof Model;
this.ins_Model_Car=ins_Model instanceof Car;
this.ins_Car_Model=ins_Car instanceof Model;
this.ins_Car_Car=ins_Car instanceof Car;
14行目でCar()を継承するまえのModel()でins_Modelをインスタンス化しています。
ins_Modelのノードから始まるプロトタイプチェーンは次のins_Model.<prototype>で途切れているのでins_Model instanceof Modelはfalseになり、当然ins_Model instanceof Carもfalseになっています。

ins_Model.<prototype>はCar.prototypeを[[Prototype]]とするオブジェクトに置換される前のModel()のプロトタイプオブジェクトです。

ins_Model.<prototype>のconstructorプロパティにはコンストラクタ関数Model()が入っていますが内部プロパティ[[Prototype]]はObject()関数のプロトタイプオブジェクトObject.prototypeが入っているので、Model.prototypeとはプロトタイプチェーンではつながっていません。

参考にしたサイト


Object Playground: The Definitive Guide to Object-Oriented JavaScript
JavaScriptプロトタイプチェーン図示ツール。

JavaScript | MDN
Mozilla Developer NetworkによるJavaScriptのマニュアル。

ECMAScript 5 compatibility table
Object.create()はすでにほとんどの環境で実装されています。

次の関連記事JavaScriptプロトタイプチェーン(5)“プロトタイプ”3種類の違いのまとめ

PR

0 件のコメント:

コメントを投稿