オブジェクト間の継承
はじめに
前の記事に続き、オブジェクトのプロトタイプについての説明が続きます。
前回はコンストラクタ関数を使ってのプロトタイプチェーンの利用について説明しました。class キーワードの中でどのような継承が行われているか、おぼろげながらでもイメージがつかめれば良いです。多分プロトタイプをガシガシ使ってコードを書くことはまずないと思われます。そのために class キーワードがあるのですから。
今回は、class キーワードの extends にあたる機能を説明したいとおもいます。オブジェクト間でプロトタイプがどのような継承をしているのかみていきましょう。
コンストラクタ関数の機能拡張
前の記事で作成した、Person コンストラクタ関数を継承して、HeirSalon コンストラクタ関数を作成します。
Person オブジェクトのプロパティ(年齢・名前)に加えて、HeirSalon オブジェクトでは新たなプロパティ(髪色・髪の長さ)を追加します。また、新たなメソッドを追加してプロファイルに髪色と髪の長さを加えたプロファイルを表示します。
コンストラクタ関数の継承
まず、前回作成した Person オブジェクトです。
let Person = function( age , fname , lname ) { this.age = age ; this.fname = fname ; this.lname = lname ; } Person.prototype.profile = function() { return( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ; let kato = new Person( 20 , 'Ippei', 'Kato' ) ; console.log( kato.profile() ) ; // name:Kato Ippei age:20
HeirSalon コンストラクタ関数を作成します。
HeirSalon オブジェクトには髪色と長さのプロパティ heirColor と heirLength を作成します。
let Person = function( age , fname , lname ) { this.age = age ; this.fname = fname ; this.lname = lname ; } Person.prototype.profile = function() { return( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ; //----- HeirSalon コンストラクタ // ****** コード追加 let HeirSalon = function( color , length , age , fname , lname ) { Person.call(this , age , fname , lname) ; // <--- Person コンストラクタ関数を this を HeirSalon オブジェクトにして展開します。 this.heirColor = color || 'Black' ; this.heirLength = length || 'Short' ; } //----- HeirSalon コンストラクタ ここまで let kato = new HeirSalon( 'Black' , 'Long' , 20 , 'Ippei', 'Kato' ) ; console.log( kato ) ; console.log( kato.profile() ) ; //<--- kato.profile is not a function でエラーとなる。
HeirSalon コンストラクタ関数から Person コンストラクタ関数を call(this,….) で実行しています。
インスタンス生成時には Person コンストラクタ関数の this は HeirSalon コンストラクタ関数と同一のコンテキストを指します。
Person.call(this , age , fname , lname) ;
kato オブジェクトの内容を確認します。
// console.log( kato ) ; の結果 HeirSalon {age: 20, fname: 'Ippei', lname: 'Kato', heirColor: 'Black', heirLength: 'Long'} age: 20 <--- Person コンストラクタ関数のプロパティが継承された。 fname: "Ippei" heirColor: "Black" heirLength: "Long" lname: "Kato" [[Prototype]]: Object constructor: ƒ ( color , length , age , fname , lname ) <--- Person の profile は継承されていない [[Prototype]]: Object
コンストラクタ関数のプロトタイプの継承
Person コンストラクタ関数のプロパティは HeirSalon コンストラクタ関数に継承されたが、プロトタイプの profile メソッドは継承されていない。
なので
console.log( kato.profile() ) ; //<--- kato.profile is not a function でエラーとなる。
kato.profile() はエラーとなってしまいます。
そこで、Person のプロトタイプを HeirSalon のプロトタイプへ追加します。Object.create() で Person.prototype の新しいオブジェクトを作成して、HeirSalon のプロトタイプとします。
HeirSalon.prototype = Object.create(Person.prototype) ;
実行をしてみましょう。
let Person = function( age , fname , lname ) { this.age = age ; this.fname = fname ; this.lname = lname ; } Person.prototype.profile = function() { return( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ; //----- HeirSalon コンストラクタ let HeirSalon = function( color , length , age , fname , lname ) { Person.call(this , age , fname , lname) ; // <--- Person コンストラクタ関数を this を HeirSalon オブジェクトにして展開します。 this.heirColor = color || 'Black' ; this.heirLength = length || 'Short' ; } // ****** コード追加 HeirSalon.prototype = Object.create(Person.prototype) ; // <--- Person コンストラクタ関数のプロトタイプを HeirSalon のプロトタイプへ //----- HeirSalon コンストラクタ ここまで let kato = new HeirSalon( 'Black' , 'Long' , 20 , 'Ippei', 'Kato' ) ; console.log( kato ) ; console.log( kato.profile() ) ; // name:Kato Ippei age:20 <--- プロトタイプが継承された
kato.profile() が正常に呼び出されました。
しかし、kato オブジェクトを確認すると、kato オブジェクトのプロトタイプのコンストラクタが Person を指しています。
(Person.prototype からプロパティを継承するオブジェクトを参照するように書き換えたので当たり前なんですけどね。)
// console.log( kato ) ; の結果 HeirSalon {age: 20, fname: 'Ippei', lname: 'Kato', heirColor: 'Black', heirLength: 'Long'} age: 20 fname: "Ippei" heirColor: "Black" heirLength: "Long" lname: "Kato" [[Prototype]]: Person [[Prototype]]: Object profile: ƒ () <--- Peoson プロトタイプの profile が参照できるようになった。 constructor: ƒ ( age , fname , lname ) arguments: null caller: null length: 3 name: "Person" <--- コンストラクタ関数が Person を示している。 [[Prototype]]: Object
この記事では問題になりませんが、Object.defineProperty() を使って HeirSalon を指すように prototype を書き換えます。
Object.defineProperty(HeirSalon.prototype, 'constructor', { value: HeirSalon, enumerable: false, // 'for in'ループで現れないようにする writable: true });
コードを入れて実行してみます。
let Person = function( age , fname , lname ) { this.age = age ; this.fname = fname ; this.lname = lname ; } Person.prototype.profile = function() { return( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ; //----- HeirSalon コンストラクタ let HeirSalon = function( color , length , age , fname , lname ) { Person.call(this , age , fname , lname) ; // <--- Person コンストラクタ関数を this を HeirSalon オブジェクトにして展開します。 this.heirColor = color || 'Black' ; this.heirLength = length || 'Short' ; } HeirSalon.prototype = Object.create(Person.prototype) ; // <--- Person コンストラクタ関数のプロトタイプを HeirSalon のプロトタイプへ // ****** コード追加 Object.defineProperty(HeirSalon.prototype, 'constructor', { value: HeirSalon, enumerable: false, // 'for in'ループで現れないようにする writable: true }); //----- HeirSalon コンストラクタ ここまで let kato = new HeirSalon( 'Black' , 'Long' , 20 , 'Ippei', 'Kato' ) ; console.log( kato ) ; console.log( kato.profile() ) ; // name:Kato Ippei age:20
kato オブジェクトの内容を確認してみます。
kato オブジェクトを確認すると、kato オブジェクトのプロトタイプのコンストラクタが HeirSalon を示しました。
// console.log( kato ) ; の結果 HeirSalon {age: 20, fname: 'Ippei', lname: 'Kato', heirColor: 'Black', heirLength: 'Long'} age: 20 fname: "Ippei" heirColor: "Black" heirLength: "Long" lname: "Kato" [[Prototype]]: Person constructor: ƒ ( color , length , age , fname , lname ) arguments: null caller: null length: 5 name: "HeirSalon" <--- コンストラクタ関数が HeirSalon を示すようになった。 [[Prototype]]: Object profile: ƒ () constructor: ƒ ( age , fname , lname ) arguments: null caller: null length: 3 name: "Person" [[Prototype]]: Object
これで、Person が HeirSalon に継承されました。
メソッドの作成
次に、プロファイルに髪の情報を追加した新しいメソッド salonProfile を作成します。
let Person = function( age , fname , lname ) { this.age = age ; this.fname = fname ; this.lname = lname ; } Person.prototype.profile = function() { return( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ; //----- HeirSalon コンストラクタ let HeirSalon = function( color , length , age , fname , lname ) { Person.call(this , age , fname , lname) ; // <--- Person コンストラクタ関数を this を HeirSalon オブジェクトにして展開します。 this.heirColor = color || 'Black' ; this.heirLength = length || 'Short' ; } HeirSalon.prototype = Object.create(Person.prototype) ; // <--- Person コンストラクタ関数のプロトタイプを HeirSalon のプロトタイプへ Object.defineProperty(HeirSalon.prototype, 'constructor', { value: HeirSalon, enumerable: false, // 'for in'ループで現れないようにする writable: true }); // ****** コード追加 HeirSalon.prototype.salonProfile = // <--- HeirSalon のプロトタイプに salonProfile として profile に髪情報を追加したメソッドを作成 function() { return( this.profile() + ' Heir:' + this.heirColor + '(' + this.heirLength + ')') ; } ; //----- HeirSalon コンストラクタ ここまで let kato = new HeirSalon( 'Black' , 'Long' , 20 , 'Ippei', 'Kato' ) ; console.log( kato ) ; console.log( kato.profile() ) ; // name:Kato Ippei age:20 console.log( kato.salonProfile() ) ; // name:Kato Ippei age:20 Heir:Black(Long)
髪情報を追加したメソッド salonProfile() が追加されました。
髪情報を追加したプロフィール「name:Kato Ippei age:20 Heir:Black(Long)」が表示されます。
salonProfile() メソッドがどのように継承されているか、kato オブジェクトを確認してみます。
// console.log( kato ) ; の結果 HeirSalon {age: 20, fname: 'Ippei', lname: 'Kato', heirColor: 'Black', heirLength: 'Long'} age: 20 fname: "Ippei" heirColor: "Black" heirLength: "Long" lname: "Kato" [[Prototype]]: Person salonProfile: ƒ () <--- メソッド salonProfile がプロトタイプへ継承された constructor: ƒ ( color , length , age , fname , lname ) [[Prototype]]: Object profile: ƒ () <--- Person の profile メソッドが継承されています constructor: ƒ ( age , fname , lname ) [[Prototype]]: Object
プロトタイプに salonProfile が作成されています。
class キーワードで書き換えてみた
HeirSalon オブジェクトを class キーワードを使って書き直すと以下のように、可読性もよく簡潔にコードが作成できます。(シンタックスシュガーなので当たり前なんですけど)
class Person { constructor( age , fname , lname ) { this.age = age ; this.fname = fname ; this.lname = lname ; } profile() { return( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ; } class HeirSalon extends Person { constructor( color , length , age , fname , lname ) { super( age , fname , lname ) ; this.heirColor = color || 'Black' ; this.heirLength = length || 'Short' ; } salonProfile() { return( this.profile() + ' Heir:' + this.heirColor + '(' + this.heirLength + ')') ; } } let kato = new HeirSalon( 'Black' , 'Long' , 20 , 'Ippei', 'Kato' ) ; console.log( kato ) ; console.log( kato.salonProfile() ) ; // name:Kato Ippei age:20 Heir:Black(Long)
// console.log( kato ) ; の結果 HeirSalon {age: 20, fname: 'Ippei', lname: 'Kato', heirColor: 'Black', heirLength: 'Long'} age: 20 fname: "Ippei" heirColor: "Black" heirLength: "Long" lname: "Kato" [[Prototype]]: Person constructor: class HeirSalon salonProfile: ƒ salonProfile() [[Prototype]]: Object constructor: class Person profile: ƒ profile() [[Prototype]]: Object
オブジェクトのプロトタイプが、どのように継承されているか知らなくてもコードを書けます。
しかし、HeirSalon クラスの constructor 内の
super( age , fname , lname ) ;
が何をしているのかは、オブジェクトの継承を知らないとピンとこないと思います。
ここまで辛抱強く記事を読まれた方は、「フムフム」と思われるはずです。
デバッグ時のコンソールに表示される [[Prototype]] を、今までぼんやりと見ていたのが、意味がわかると視野が広がります。これはプログラムの開発時に非常に役立ちます。
そして、クラス設計の品質も向上します。(これ重要です)
この記事が、JavaScript オブジェクトのプロトタイプの壁を乗り越えるための、一助になればよいと願っています。
参考リンク
MDN > 初心者のためのオブジェクト指向 JavaScript
MDN > JavaScriptオブジェクト入門 > JavaScript での継承
MDN 開発者向けのウェブ技術 > 継承とプロトタイプチェーン
MDN 開発者向けのウェブ技術 > 標準組み込みオブジェクト > Object.defineProperty()
MDN 開発者向けのウェブ技術 > extends
MDN 開発者向けのウェブ技術 > クラス
MDN 開発者向けのウェブ技術 > JavaScript「再」入門