前の関連記事: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();抽象的な内容のオブジェクトを介することなく同じ結果が得られました。
但しカプセル化はアンダースコアで始まるプロパティ名には外部からアクセスしないというコーディングルールに頼っています。
const myCar = { _brand: "toyota", _drive: "2WD", getBrand(){ return this._brand; } }; const yourCar = Object.assign(Object.create(myCar), { _brand: "subaru", _drive: "4WD", getModel(){ 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();ES6から導入されたObject.assign()を使うとオブジェクトリテラルでプロパティを新しいオブジェクトに渡せます。
ES6で使えるようになったconstをvarに代わって使っています。
またfunctionという表記を省略しています。
プロトタイプチェーンでつながないと関数が共有されない
そもそも他のオブジェクトのプロパティに入っている関数を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は別のオブジェクトであることがよくわかります。
Object.assign()でプロパティをオブジェクトからオブジェクトにコピーする
(2018.3.16追記。例がわかりにくかったので書き直しました。)
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); // myCarのプロパティをyourCarにコピーする。 //図示化用コード this.myCar=myCar; this.yourCar=yourCar;
13行目でmyCarのプロパティをyourCarにコピーしています。
myCarの3つのプロパティ_brand、_drive、getBrandがyourCarにコピーされおり、getBrandの関数は同じものが使いまわされていることがわかります。
Object.assign() - JavaScript | MDNに使い方の例がいくつかあります。
コピーされるのはオブジェクトの直接のプロパティのみで、プロトタイプチェーン上のプロパティはコピーされません。
関数型継承パターンでカプセル化を実現する
これもプロトタイプ・ベースのオブジェクト指向プログラミングを採り入れるで紹介されていた方法です。
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();カプセル化したいデータをオブジェクトにして関数の引数で受け取り、関数以外のプロパティの値をもたないオブジェクトを返すことでカプセル化を実現しています。
継承は関数以外のプロパティの値をもたないオブジェクトを渡すことで実現しています。
この方法で作成したオブジェクトのプロパティはプロトタイプチェーンで使い回しされていません。
CarやModelは関数なのでObject.create()でプロトタイプチェーンをつなぐことはできませんし、Carは呼び出されるたびに新たにオブジェクトを作成している(2行目)ので、それぞれの戻り値をObject.create()してもプロトタイプチェーンではつながりません。
カプセル化は名前空間の作成のためのモジュール化するときに使う程度なので、継承する機会はいまのところ思いあたりません。
参考にしたサイト
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() - JavaScript | MDN
プロパティをオブジェクトからオブジェクトにコピーするメソッド。
プロトタイプ・ベースのオブジェクト指向プログラミングを採り入れる
クラスベースと対比してプロトタイプベースのプログラミング方法が解説されています。
Aadit M Shah | Why Prototypal Inheritance Matters
プロトタイプベースのプログラミング方法の追究。
Fluent JavaScript – Three Different Kinds of Prototypal OO | My Blog
これもプロトタイプベースのプログラミング方法の追究ですが独自のライブラリを使用する方法です。
プロトタイプを指定してオブジェクトを作成する方法の速度比較。Object.create()とConstructorが最適化されていることがわかります。
「JavaScriptと性能についての本当の話」をしよう。ダグラス・クロックフォード氏 - Publickey
速度測定は意味がない?
Prototypal Inheritance
JavaScript と Scheme について - ksmakotoのhatenadiary
JavaScript開発時の経緯からJavaと同じnew演算子が導入されたようです。
JS history
JavaScriptの歴史がわかるおもしろいスライドです。
オブジェクト指向 JavaScript 入門 - JavaScript | MDN
プロトタイプベースであるJavaScriptでクラスを模倣する解説。
Common Misconceptions About Inheritance in JavaScript
Prototypal Inheritanceの解説。
0 件のコメント:
コメントを投稿