JavaScript 再入門(その16) オブジェクト 3 コンストラクタ関数

コンストラクタ関数

はじめに

JavaScript 再入門(その15) オブジェクト 2 継承とプロトタイプチェーン
継承とプロトタイプチェーン はじめに これからオブジェクトのプロトタイプについての説明が続きますが、前の記事でも書いたように、ES2015 以降 class キ…
sakura-system.com

前の記事に続き、オブジェクトのプロトタイプについての説明が続きます。
前回は継承とプロトタイプチェーンの動作が理解しやすいように、オブジェクトを Object.create() を使って説明しました。今回はコンストラクタ関数を使ってのプロトタイプチェーンの利用です。より class キーワードを使ったクラスベースに近い説明となるので、 プロトタイプベース言語(厳密にはプロトタイプに基づいたオブジェクトベースの言語) への理解の助けになるのではないでしょうか。

コンストラクタ関数

JavaScript では、オブジェクトやその機能を定義し初期化するためにコンストラクタ関数と呼ばれる特殊な関数を使用します。

JavaScriptのコンストラクタ関数の流儀として以下の2点に注意してください。

  1. 名前は大文字で始める
  2. new により作成されたインスタンスによってのみ実行される

前の記事で使った person オブジェクトを元にコンストラクタ関数 Person を作成します。

let Person = function( age , fname , lname ) {
	this.age = age ;
	this.fname = fname ;
	this.lname = lname ;
	this.profile = function() { return( 'name:' +  this.lname + ' ' + this.fname + ' age:' +  this.age ) ; } ;
}
let kato = new Person( 20 , 'Ippei', 'Kato' ) ; // Person コンストラクタ関数から kato オブジェクトのインスタンスを生成
console.log( typeof(Person) ) ; // function
console.log( typeof(kato) ) ; // object
console.log( kato ) ;
console.log( kato.profile() ) ; // name:Kato Ippei age:20

Person コンストラクタ関数から kato オブジェクトのインスタンスを生成しています。
kato.profile() も想定通りの結果を返します。

typeof() で Person と kato の型を確認してみます。
Person —- function
kato —- object
Person コンストラクタ関数 (function) から kato オブジェクト(object) が生成されています。

但しこのインスタンスには問題があります。
katoの内容を確認します。

// console.log( kato ) ; の結果
Person {age: 10, fname: 'Ippei', lname: 'Kimura', eye: 'Black', profile: ƒ}
	age: 10
	fname: "Ippei"
	lname: "Kimura"
	profile: ƒ ()  <--- profile プロパティ(実態はメソッド)も生成されている。
	[[Prototype]]: Object
		constructor: ƒ ( age , fname , lname )
		[[Prototype]]: Object

コンストラクタ関数に定義されている profile プロパティ(実態はメソッド)も function として生成されています。
(コンストラクタ関数に定義したのだから当たり前なんですけどね。)
例えば続けて以下のようにインスタンスを生成していきます。

let kimura = new Person( 25 , 'Taro', 'Kimura' ) ;
let yoshida = new Person( 30 , 'Jiro', 'Yoshida' ) ;

とすると、kato / kimura / yoshida オブジェクトそれぞれに全く同じ profile プロパティが生成されます。

Person {age: 20, fname: 'Ippei', lname: 'Kato', profile: ƒ}  <--- それぞれに profile が生成されている。
Person {age: 25, fname: 'Taro', lname: 'Kimura', profile: ƒ}
Person {age: 30, fname: 'Jiro', lname: 'Yoshida', profile: ƒ}

これを、JavaScript 以外のプログラミングを習得している方にもわかりやすいように、class キーワードを使って書き直してみます。

class Person {
	constructor( age , fname , lname ) {
		this.age = age ;
		this.fname = fname ;
		this.lname = lname ;
		this.profile = function() { return( 'name:' +  this.lname + ' ' + this.fname + ' age:' +  this.age ) ; } ; //
	}
}
let kato = new Person( 20 , 'Ippei', 'Kato' ) ; // name:Kato Ippei age:20

コンストラクタにメソッドを入れることに違和感がありますよね。

Person {age: 20, fname: 'Ippei', lname: 'Kato', profile: ƒ} <--- profile が生成されている

メソッドをコンストラクターに定義する事は通常ありません、コンストラクタ外に定義してメソッドは継承すればよいのですから。

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 ) ; } ; // メソッドとして定義
}
let kato = new Person( 20 , 'Ippei', 'Kato' ) ;
console.log( kato.profile() ) ; // name:Kato Ippei age:20
console.log( kato ) ;

kato オブジェクトの内容を確認してみます。

// console.log( kato ) ; の結果
Person {age: 20, fname: 'Ippei', lname: 'Kato'}  <--- コンストラクタから profile メソッドがなくなった
	age: 20
	fname: "Ippei"
	lname: "Kato"
	[[Prototype]]: Object
		constructor: class Person
		profile: ƒ profile()   <--- メソッド profile がプロトタイプチェーンによって継承されている。
		[[Prototype]]: Object

では、class キーワードを使わない場合はどのようにメソッドの継承が行われているかというと。

let Person = function( age , fname , lname ) {
	this.age = age ;
	this.fname = fname ;
	this.lname = lname ;
}
Person.prototype.profile = // <--- Person コンストラクタ関数のプロトタイプに 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
console.log( kato ) ;

Person コンストラクタ関数のプロトタイプに profile メソッドを追加します。

コンストラクタ関数のプロトタイプに登録することで、プロトタイプチェーンを使った継承が行われます。

kato オブジェクトを確認してみます。

// console.log( kato ) ; の結果
Person {age: 20, fname: 'Ippei', lname: 'Kato'}  <--- コンストラクターから profile メソッドがなくなった
	age: 20
	fname: "Ippei"
	lname: "Kato"
	[[Prototype]]: Object
		profile: ƒ ()    <--- メソッド profile がプロトタイプチェーンによって継承されている。
		constructor: ƒ ( age , fname , lname )
		[[Prototype]]: Object

多くのオブジェクト定義でよく見られるパターンは、コンストラクタ内でプロパティを定義し、プロトタイプ上でメソッドを定義するパターンです。

プロパティに初期値を設定する。

前の記事の person オブジェクトは初期値が設定されていました。
コンストラクタ関数でも初期値を設定することができます。

let Person = function( age = 10 , fname = 'Ippei' , lname = 'Kimura' ) {
	this.age = age ;
	this.fname = fname ;
	this.lname = lname ;
}
Person.prototype.profile =
	function() {
		return( 'name:' +  this.lname + ' ' + this.fname + ' age:' +  this.age ) ;
	} ;

let kimura = new Person ;
console.log( kimura.profile() ) ; // name:Kimura Ippei age:10

または

let Person = function( age , fname , lname ) {
	this.age = age || 10 ;
	this.fname = fname || 'Ippei' ;
	this.lname = lname || 'Kimura' ;
}
Person.prototype.profile =
	function() {
		return( 'name:' +  this.lname + ' ' + this.fname + ' age:' +  this.age ) ;
	} ;

let kimura = new Person ;
console.log( kimura.profile() ) ; // name:Kimura Ippei age:10

参考リンク

MDN > 初心者のためのオブジェクト指向 JavaScript
MDN 開発者向けのウェブ技術 > 標準組み込みオブジェクト > Function
MDN 開発者向けのウェブ技術 > クラス
MDN 開発者向けのウェブ技術 > JavaScript「再」入門