前の関連記事:JavaScriptプロトタイプチェーン(5)“プロトタイプ”3種類の違いのまとめ
前々回やったクラスの継承(Classical Inheritance)に対してプロトタイプの継承(Prototypal Inheritance)というのがあります。ちょっとしたプログラムを書くにはクラスを使うよりもプロトタイプの継承だけで済ませた方が簡単かもしれません。
クラスみたいな抽象的なオブジェクトを自分で作って継承する方法
Object Playgroundの解説ビデオの14:40から紹介されている方法です。
1 2 3 4 5 6 7 8 9 10 |
var Car = { init: function (brand, drive) { this ._brand = brand; this ._drive = drive; }, getBrand: function (){ return this ._brand; } }; this .Car=Car; //図示化用コード |
まずは抽象的な内容のオブジェクトCarを作成します。
initプロパティに初期化のための関数を入れています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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(); |
10行目でCarオブジェクトのinitプロパティの関数でmyCarのプロパティに値を設定しています。
Carオブジェクトはthis.myCar.<prototype>というラベル名になっています。
プロトタイプチェーンに従ってmyCar.getBrand()に対して"toyota"の値で返っています。
今度はCarを継承したModelという抽象的な内容のオブジェクトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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; |
14行目ではCarのgetBrandプロパティに入っている関数オブジェクトにthisをcall()で渡しています。(WSH JScriptでJavaScriptのお勉強(関数定義、クロージャ、this))
Car.getBrandは14行目では関数オブジェクトの単なる住所であって継承関係などは関係ありません。(メソッドの束縛)
今度はModelからObject.create() で新たなオブジェクトを作成してinitメソッドで具体的な値をプロパティに設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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(); |
Modelオブジェクトはthis.yourCar.<prototype>というラベル名になっています。
yourCarオブジェクトのプロトタイプチェーンはCarオブジェクト(グラフのラベル名はthis.myCar.<prototype>)へつながっておりgetBrandメソッドが使えることがわかります。
Object Playgroundの解説ビデオの14:40から紹介されているこの方法は、クラスの継承(Classical Inheritance)に対するプロトタイプの継承(Prototypal Inheritance)として紹介されていたものですが、オブジェクトを抽象的なものと具体的なものの2つの世界に分けています。
二つの世界にわけてしまうと思考方法としてはLibreOffice(10)オブジェクト指向プログラミングのお勉強:総論でみたようにクラスベースと変わりなくなってしまいます。
よりプロトタイプベースらしい継承パターン
プロトタイプ・ベースのオブジェクト指向プログラミングを採り入れるに書いてある方法です。
最初から具体的なオブジェクトmyCarを作ります。
1 2 3 4 5 6 7 8 9 10 |
var myCar = { _brand: "toyota" , _drive: "2WD" , getBrand: function (){ return this ._brand; } }; //図示化用コード this .myCar=myCar; this .myCar_getBrand=myCar.getBrand(); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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(); |
但しカプセル化はアンダースコアで始まるプロパティ名には外部からアクセスしないというコーディングルールに頼っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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で使えるようになったconstをvarに代わって使っています。
またfunctionという表記を省略しています。
プロトタイプチェーンでつながないと関数が共有されない
そもそも他のオブジェクトのプロパティに入っている関数をcall(this)で使えるのなら継承しなくても使いたいメソッドだけcall(this)で呼び出してしまえばよいわけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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(); |
でも別の関数として定義しているので、「Show all function」にチェックしてオブジェクトビジュアライザーで図示してみると、myCarのgetBrandとyourCarのgetBrandは別のオブジェクトであることがよくわかります。
Object.assign()でプロパティをオブジェクトからオブジェクトにコピーする
(2018.3.16追記。例がわかりにくかったので書き直しました。)
ES6で定義されたObject.assign()を使うと同じプロパティを使い回しできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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に使い方の例がいくつかあります。
コピーされるのはオブジェクトの直接のプロパティのみで、プロトタイプチェーン上のプロパティはコピーされません。
関数型継承パターンでカプセル化を実現する
これもプロトタイプ・ベースのオブジェクト指向プログラミングを採り入れるで紹介されていた方法です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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 件のコメント:
コメントを投稿