JavaScript 再入門(その17) オブジェクト 4 オブジェクト間の継承

オブジェクト間の継承

はじめに

JavaScript 再入門(その16) オブジェクト 3 コンストラクタ関数
コンストラクタ関数 はじめに 前の記事に続き、オブジェクトのプロトタイプについての説明が続きます。前回は継承とプロトタイプチェーンの動作が理解しやすいように、オ…
sakura-system.com

前の記事に続き、オブジェクトのプロトタイプについての説明が続きます。
前回はコンストラクタ関数を使ってのプロトタイプチェーンの利用について説明しました。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「再」入門