JavaScriptプロトタイプチェーン(6)プロトタイプの継承パターン(Prototypal Inheritance)

ラベル:

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


前々回やったクラスの継承(Classical Inheritance)に対してプロトタイプの継承(Prototypal Inheritance)というのがあります。ちょっとしたプログラムを書くにはクラスを使うよりもプロトタイプの継承だけで済ませた方が簡単かもしれません。

クラスみたいな抽象的なオブジェクトを自分で作って継承する方法


Object Playgroundの解説ビデオの14:40から紹介されている方法です。
var Car = {
 init: function(brand, drive) {
  this._brand = brand;
  this._drive = drive;
 },
 getBrand: function(){
  return this._brand;
 }
};
this.Car=Car;//図示化用コード
(最後の行のthisはObject Playgroundのオブジェクトビジュアライザーでグラフ表示に使うオブジェクトです。JavaScriptプロトタイプチェーン(1)プロトタイプチェーン図示ツール

まずは抽象的な内容のオブジェクトCarを作成します。
initプロパティに初期化のための関数を入れています。
var Car = {
 init: function(brand, drive) {
  this._brand = brand;
  this._drive = drive;
 },
 getBrand: function(){
  return this._brand;
 }
};
var myCar = Object.create(Car);
myCar.init("toyota", "2WD");
//図示化用コード
this.myCar=myCar;
this.myCar_getBrand=myCar.getBrand();
9行目で抽象的な内容のオブジェクトCarを内部プロパティ[[Prototype]]にもつオブジェクトmyCarを作成しています。

10行目でCarオブジェクトのinitプロパティの関数でmyCarのプロパティに値を設定しています。
Carオブジェクトはthis.myCar.<prototype>というラベル名になっています。

プロトタイプチェーンに従ってmyCar.getBrand()に対して"toyota"の値で返っています。

今度はCarを継承したModelという抽象的な内容のオブジェクトを作成します。
var Car = {
 init: function(brand, drive) {
  this._brand = brand;
  this._drive = drive;
 },
 getBrand: function(){
  return this._brand;
 }
};
var myCar = Object.create(Car);
myCar.init("toyota", "2WD");
var Model = Object.create(Car);
Model.getModel = function(){
 return Car.getBrand.call(this) + this._drive;
};
//図示化用コード
this.myCar=myCar;
this.myCar_getBrand=myCar.getBrand();
this.Model=Model;
12行目でCarを内部プロパティ[[Prototype]]に設定して新たなオブジェクトModelを作成しました。

14行目ではCarのgetBrandプロパティに入っている関数オブジェクトにthisをcall()で渡しています。(WSH JScriptでJavaScriptのお勉強(関数定義、クロージャ、this))

Car.getBrandは14行目では関数オブジェクトの単なる住所であって継承関係などは関係ありません。(メソッドの束縛)

今度はModelからObject.create() で新たなオブジェクトを作成してinitメソッドで具体的な値をプロパティに設定します。
var Car = {
 init: function(brand, drive) {
  this._brand = brand;
  this._drive = drive;
 },
 getBrand: function(){
  return this._brand;
 }
};
var myCar = Object.create(Car);
myCar.init("toyota", "2WD");
var Model = Object.create(Car);
Model.getModel = function(){
 return Car.getBrand.call(this) + this._drive;
};
var yourCar = Object.create(Model);
yourCar.init("subaru", "4WD");

//図示化用コード
this.myCar=myCar;
this.myCar_getBrand=myCar.getBrand();
this.yourCar=yourCar;
this.yourCar_getBrand=yourCar.getBrand();
this.yourCar_getModel=yourCar.getModel();
16行目でyourCarというオブジェクトをModelオブジェクトから作成しています。

Modelオブジェクトはthis.yourCar.<prototype>というラベル名になっています。

yourCarオブジェクトのプロトタイプチェーンはCarオブジェクト(グラフのラベル名はthis.myCar.<prototype>)へつながっておりgetBrandメソッドが使えることがわかります。

Object Playgroundの解説ビデオの14:40から紹介されているこの方法は、クラスの継承(Classical Inheritance)に対するプロトタイプの継承(Prototypal Inheritance)として紹介されていたものですが、オブジェクトを抽象的なものと具体的なものの2つの世界に分けています。

二つの世界にわけてしまうと思考方法としてはLibreOffice(10)オブジェクト指向プログラミングのお勉強:総論でみたようにクラスベースと変わりなくなってしまいます。

よりプロトタイプベースらしい継承パターン


プロトタイプ・ベースのオブジェクト指向プログラミングを採り入れるに書いてある方法です。

最初から具体的なオブジェクトmyCarを作ります。
var myCar = {
 _brand: "toyota",
 _drive: "2WD",
 getBrand: function(){
  return this._brand;
 }
};
//図示化用コード
this.myCar=myCar;
this.myCar_getBrand=myCar.getBrand();
このmyCarオブジェクトからyourCarオブジェクトを作成します。
var myCar = {
 _brand: "toyota",
 _drive: "2WD",
 getBrand: function(){
  return this._brand;
 }
};
var yourCar = Object.create(myCar);
yourCar._brand = "subaru"
yourCar._drive = "4WD"
yourCar.getModel = function(){
 return myCar.getBrand.call(this) + this._drive;
};
//図示化用コード
this.myCar=myCar;
this.myCar_getBrand=myCar.getBrand();
this.yourCar=yourCar;
this.yourCar_getBrand=yourCar.getBrand();
this.yourCar_getModel=yourCar.getModel();
抽象的な内容のオブジェクトを介することなく同じ結果が得られました。

但しカプセル化はアンダースコアで始まるプロパティ名には外部からアクセスしないというコーディングルールに頼っています。

そもそも他のオブジェクトのプロパティに入っている関数をcall(this)で使えるのなら継承しなくても使いたいメソッドだけcall(this)で呼び出してしまえばよいわけです。
var myCar = {
 _brand: "toyota",
 _drive: "2WD",
 getBrand: function(){
  return this._brand;
 }
};
var yourCar = {
 _brand: "subaru",
 _drive: "4WD",
 getBrand: function(){
  return myCar.getBrand.call(this);
 },
 getModel: function(){
  return myCar.getBrand.call(this) + this._drive;
 }
};
//図示化用コード
this.myCar=myCar;
this.myCar_getBrand=myCar.getBrand();
this.yourCar=yourCar;
this.yourCar_getBrand=yourCar.getBrand();
this.yourCar_getModel=yourCar.getModel();
12行目と15行目でmyCarのプロパティの関数を呼び出しています。

でも別の関数として定義しているので、「Show all function」にチェックしてオブジェクトビジュアライザーで図示してみると、myCarのgetBrandとyourCarのgetBrandは別のオブジェクトであることがよくわかります。

ES6で定義されたObject.assign()を使うと同じプロパティを使い回しできます。
var myCar = {
 _brand: "toyota",
 _drive: "2WD",
 getBrand: function(){
  return this._brand;
 }
};
var yourCar = {
 getModel: function(){
  return myCar.getBrand.call(this) + this._drive;
 }
};
Object.assign(yourCar, myCar);
yourCar._brand = "subaru"
yourCar._drive = "4WD"
//図示化用コード
this.myCar=myCar;
this.myCar_getBrand=myCar.getBrand();
this.yourCar=yourCar;
this.yourCar_getBrand=yourCar.getBrand();
this.yourCar_getModel=yourCar.getModel(); 
14行目と15行目のプロパティの設定をもっとかっこよく1発でやりたいといろいろ調べてみましたが思いつきませんでした。

 関数型継承パターンでカプセル化を実現する


これもプロトタイプ・ベースのオブジェクト指向プログラミングを採り入れるで紹介されていた方法です。
var Car = function(spec) {
    var that = {};
    that.getBrand = function() {
        return spec.brand;
    };
    that.setBrand = function(brand) {
        spec.brand = brand;
    };
    that.getDrive = function() {
        return spec.drive;
    };
    that.setDrive = function(drive) {
        spec.drive = drive;
    };
    return that;
};
var Model = function(spec) {
    var that = Car(spec);
    that.getModel = function() {
        return spec.brand + spec.drive;
    };
    return that;
};
var myCar = Car({brand: "toyota", drive: "2WD"});
var yourCar = Model({brand: "subaru", drive: "4WD"});
//図示化用コード
this.myCar=myCar;
this.myCar_getBrand=myCar.getBrand();
this.myCar_getModel=myCar.getDrive();
this.yourCar=yourCar;
this.yourCar_getBrand=yourCar.getBrand();
this.yourCar_getDrive=yourCar.getDrive();
this.yourCar_getModel=yourCar.getModel();
カプセル化したいデータをオブジェクトにして関数の引数で受け取り、関数以外のプロパティの値をもたないオブジェクトを返すことでカプセル化を実現しています。

継承は関数以外のプロパティの値をもたないオブジェクトを渡すことで実現しています。

参考にしたサイト


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

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

ECMAScript 6 compatibility table
Object static methodsにObject.assign()があります。

プロトタイプ・ベースのオブジェクト指向プログラミングを採り入れる
クラスベースと対比してプロトタイプベースのプログラミング方法が解説されています。

Aadit M Shah | Why Prototypal Inheritance Matters
プロトタイプベースのプログラミング方法の追究。

Fluent JavaScript – Three Different Kinds of Prototypal OO « Eric Elliott – JavaScript Architect (A JavaScript Blog)
これもプロトタイプベースのプログラミング方法の追究ですが独自のライブラリを使用する方法です。

Object.createとnew演算子の速度の比較ができます。表の左の項目をクリックすると個別に速度測定ができます。

「JavaScriptと性能についての本当の話」をしよう。ダグラス・クロックフォード氏 - Publickey
速度測定は意味がない?

Prototypal Inheritance
Crockford Createの開発者のブログ。Object.createの変遷がわかります。

JavaScript と Scheme について - ksmakotoのhatenadiary
JavaScript開発時の経緯からJavaと同じnew演算子が導入されたようです。

JS history
JavaScriptの歴史がわかるおもしろいスライドです。

オブジェクト指向 JavaScript 入門 - JavaScript | MDN
プロトタイプベースであるJavaScriptでクラスを模倣する解説。
PR

0 件のコメント:

コメントを投稿