吉本集の個人ブログ
Web制作の技術について書いています。たまに日記も書きます。

【JS入門】オブジェクトの作り方 Vol.2(コンストラクタ関数・プロパティの継承)

2018年1月31日 / category : javascript

前回の記事では、オブジェクト初期化子を使用したオブジェクト生成について解説しました。今回は、コンストラクタ関数でオブジェクトを生成することについて解説していきます。

コンストラクタ関数を使用したオブジェクト生成

コンストラクタ関数については、過去の記事「コンストラクタ関数について」で解説していますので、既に理解しているという前提で進めていきます。
次のコードを見てみます。

sample.js
function Text(lineHeight=18){
	this.lineHeight = lineHeight;
	this.setLineHeight = function(value){
		this.lineHeight = value;
	};
};
let t = new Text();
console.log( t.lineHeight ); // 18
t.setLineHeight(20);
console.log( t.lineHeight ); // 20

上のコードを簡単に解説します。Textオブジェクトを定義し、その中でプロパティlineHeight、メソッドsetLineHeightを定義しています。プロパティlineHeightは行間の数値、メソッドsetLineHeightは行間の数値を設定する、とイメージしていただければ良いかもしれません。
変数tにTextオブジェクトのインスタンスを生成し、プロパティlineHeightを参照すると、デフォルト値である18が返ってきています。
続いて、メソッドsetLineHeightを実行し、プロパティlineHeightに20を再代入しています。
問題なく動作しておりますが、あまり推奨されません。

では、なぜ推奨されないかを次のコードで確認します。

sample.js
function Text(lineHeight=18){
	this.lineHeight = lineHeight;
	this.setLineHeight = function(value){
		this.lineHeight = value;
	};
};
let t1 = new Text(20);
let t2 = new Text(22);
let t3 = new Text(24);
console.log( t1 ); // Text {lineHeight: 20, setLineHeight: ƒ}
console.log( t2 ); // Text {lineHeight: 22, setLineHeight: ƒ}
console.log( t3 ); // Text {lineHeight: 24, setLineHeight: ƒ}

Textオブジェクトのインスタンスを3個作りました。それぞれt1,t2,t3としています。これらをコンソールで出力すると、プロパティlineHeightと、メソッドsetLineHeightがそれぞれ定義されています。プロパティlineHeightに関しては、それぞれ異なる値が代入されているので問題ありませんが、メソッドsetLineHeightの関数文はどれも同じです。言い換えると、同じ内容のメソッドをインスタンスを生成する度に定義してしまっていることになります。これはメモリを無駄に使っているため推奨できません。次のように書き換えます。

sample.js
function Text(lineHeight=18){
	this.lineHeight = lineHeight;
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
let t1 = new Text(20);
let t2 = new Text(22);
let t3 = new Text(24);
console.log( t1 ); // Text {lineHeight: 20}
console.log( t2 ); // Text {lineHeight: 22}
console.log( t3 ); // Text {lineHeight: 24}
t1.setLineHeight(30);
t2.setLineHeight(32);
t3.setLineHeight(34);
console.log( t1.lineHeight ); // 30
console.log( t2.lineHeight ); // 32
console.log( t3.lineHeight ); // 34

Textオブジェクトに定義してあったメソッドsetLineHeightを、Textオブジェクトのプロトタイプのメソッドに定義しました。
インスタンス(t1,t2,t3)を参照すると、メソッドsetLineHeightがなくなっていますが、メソッドsetLineHeightは問題なく実行されています。
メソッドsetLineHeightがどこで定義しているか確認してみます。

sample.js
function Text(lineHeight=18){
	this.lineHeight = lineHeight;
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
let t1 = new Text(20);
let t2 = new Text(22);
let t3 = new Text(24);
console.log( t1.__proto__ ); // {setLineHeight: ƒ, constructor: ƒ}
console.log( t2.__proto__ ); // {setLineHeight: ƒ, constructor: ƒ}
console.log( t3.__proto__ ); // {setLineHeight: ƒ, constructor: ƒ}

インスタンス(t1,t2,t3)のプロトタイプにメソッドsetLineHeightが定義されていることが確認できました。これはプロトタイプチェーンの機能が働き問題なく実行されます。プロパティ__proto__はプロトタイプを参照しています。インスタンスを生成した際に、新たに定義されているわけではありません。参照とは、代入されている値(データ)を取得する処理ですので、新たにメモリが増えるわけではありません。
よって、メソッドを定義する際は、オブジェクトの関数文に定義するのではなく、プロトタイプに定義した方が好ましいです。

続いて、プロパティの継承について、解説したいと思います。

プロパティの継承 : Object.setPrototypeOf()

あるオブジェクトに別のオブジェクトのプロパティをオーバーライド(上書き)したいケースが出てきます。それをプロパティを継承するといいますが、その方法としてES6からObjectオブジェクトにメソッドsetPrototypeOfが追加されました。ES6以前の方法もありますので、その方法も合わせて紹介します。

sample.js
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
};
let f = new Font();
console.log( f ); // Font {color: "#000", size: 14, weight: false}

Fontオブジェクトを定義しました。変数fは、Fontオブジェクトのインスタンスです。Fontオブジェクトは、プロパティcolor、size、weightを保持しています。それぞれ、フォントのカラー、サイズ、ウエイトの属性とイメージしてください。プロパティweightには、条件 (三項) 演算子を使用していますので、こちらはMDNのページで確認してください。
インスタンスの生成時にそれぞれのプロパティの値に初期値が代入されているのが確認できます。それでは、このFontオブジェクトのプロパティを先ほどのTextオブジェクトに継承させてみます。

sample.js
function Text(lineHeight=18){
	this.lineHeight = lineHeight;
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
};
Object.setPrototypeOf( Text.prototype, Font.prototype );
console.log( Text.prototype ); // Font {setLineHeight: ƒ, constructor: ƒ}

ObjectオブジェクトのメソッドsetPrototypeOfの引数には、設定されるプロトタイプ(第一引数)、継承させる(新しい)プロトタイプ(第二引数)を指定します。まず、メソッドsetPrototypeOfを実行前のTextオブジェクトのプロトタイプを次に記述します。

{setLineHeight: ƒ, constructor: ƒ}
	setLineHeight:ƒ (value)
	constructor:ƒ Text(lineHeight=18)
	__proto__:
		constructor:ƒ Object()

続いて、メソッドsetPrototypeOfの実行後のTextオブジェクトのプロトタイプを次に記述します。

Font {setLineHeight: ƒ, constructor: ƒ}
	setLineHeight:ƒ (value)
	constructor:ƒ Text(lineHeight=18)
	__proto__:
		constructor:ƒ Font(color='#000', size=14, weight=false)
		__proto__:Object

メソッドsetPrototypeOfの実行後のTextオブジェクトのプロトタイプを見ると、Fontオブジェクトのプロパティが継承されていることが確認できました。それではこの状態でTextオブジェクトのインスタンスを生成してみます。

sample.js
function Text(lineHeight=18){
	this.lineHeight = lineHeight;
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
};
Object.setPrototypeOf( Text.prototype, Font.prototype );
let t = new Text();
console.log( t ); // Text {lineHeight: 18}

インスタンスを生成しましたが、Fontオブジェクトのプロパティcolor、size、weightの初期値が代入されていません。インスタンスのコンソールを展開してみます。

Text {lineHeight: 18}
	lineHeight:18
	__proto__:Font
		setLineHeight:ƒ (value)
		constructor:ƒ Text(lineHeight=18)
		__proto__:
			constructor:ƒ Font(color='#000', size=14, weight=false)
			__proto__:Object

展開すると、Fontオブジェクトのプロパティが問題なく継承されていることが確認できます。では、なぜプロパティの初期値が代入されないのでしょうか。
これはコンストラクタ関数Fontが実行されていないことが原因です。コンソールを設定し、確認してみます。

sample.js
function Text(lineHeight=18){
	this.lineHeight = lineHeight;
	console.log( 'Text' ); // 実行される
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
	console.log( 'Font' ); // 実行されない
};
Object.setPrototypeOf( Text.prototype, Font.prototype );
let t = new Text();

Textオブジェクト内に記述したコンソールは実行されましたが、Fontオブジェクト内に記述したコンソールは実行されませんでした。これで、コンストラクタ関数Textが実行され、コンストラクタ関数Fontが実行されていないことが確認できました。コンストラクタ関数が実行された際に、継承されているコンストラクタ関数も連動して実行されるためには、Functionオブジェクトで定義してあるメソッドcallを使用します。

sample.js
function Text(lineHeight=18){
	Font.call(this);
	this.lineHeight = lineHeight;
	console.log( 'Text' ); // 実行される
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
	console.log( 'Font' ); // 実行される
};
Object.setPrototypeOf( Text.prototype, Font.prototype );
let t = new Text();
console.log( t ); // Text {color: "#000", size: 14, weight: false, lineHeight: 18}

FontオブジェクトはFunctionオブジェクトを継承していますので、メソッドcallを使用することができます。メソッドcallの第一引数には、Fontオブジェクト内で使用するthisの値を指定します。Fontオブジェクト内で使用するthisは、Textオブジェクトになりますので、第一引数にはthis(Textオブジェクト)を設定します。このthisについては、過去の記事でも触れましたが、改めてthisについての記事を書きます。
生成したインスタンスを確認するとFontオブジェクトから継承したプロパティcolor、size、weightに初期値が代入されていることがわかります。
それでは、この初期値に異なる値を代入してみます。

sample.js
function Text(lineHeight=18,color='#000', size=14, weight=false){
	Font.call(this,color,size,weight);
	this.lineHeight = lineHeight;
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
};
Object.setPrototypeOf( Text.prototype, Font.prototype );
let t = new Text(18, '#0f0', 16, true);
console.log( t ); // Text {color: "#0f0", size: 16, weight: true, lineHeight: 18}

上の赤字の部分が追加した引数です。コンストラクタ関数Textに引数を4つ指定しています。最初の引数はTextオブジェクトのプロパティlineHeightの値になります。それ以降の3つは、Fontオブジェクトのプロパティにあたります。メソッドcallの第二引数以降は、対象のオブジェクトの引数になります。
以上が、メソッドsetPrototypeOfを使用したプロパティの継承になります。

続いて、メソッドsetPrototypeOfを使用しないでプロパティを継承する方法を紹介します。

プロパティの継承 : Object.create()

過去の記事「オブジェクトの作り方 Vol.1(オブジェクト初期化子)」で一度出てきました。Objectオブジェクトのメソッドcreateは、継承するメソッドではなくオブジェクト(プロトタイプオブジェクト)を生成するメソッドです。これを使用してプロパティを継承します。まずは、次のコードを見てみます。

sample.js
function Text(lineHeight=18,color='#000', size=14, weight=false){
	Font.call(this,color,size,weight);
	this.lineHeight = lineHeight;
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
};
Text.prototype = Object.create(Font.prototype);
let t = new Text(18, '#0f0', 16, true);
console.log( t ); // Text {color: "#0f0", size: 16, weight: true, lineHeight: 18}
t.setLineHeight(20); // TypeError: t.setLineHeight is not a function

インスタンスを確認すると、プロパティcolor、size、weightに値が代入されており問題なく継承されているように見えますが、Textオブジェクトのプロトタイプで定義したメソッドsetLineHeightを実行するとエラーになってしまいます。
これはObjectオブジェクトのメソッドcreateがプロパティを継承する処理ではなく、オブジェクト(プロトタイプオブジェクト)を生成する処理のため、生成したオブジェクト(プロトタイプオブジェクト)をTextオブジェクトのプロトタイプオブジェクトに代入し、元のプロトタイプオブジェクトは上書きされてしまい、メソッドsetLineHeightがなくなってしまっている状態です。これを回避するため、メソッドcreateで定義し直す必要があります。

sample.js
function Text(lineHeight=18,color='#000', size=14, weight=false){
	Font.call(this,color,size,weight);
	this.lineHeight = lineHeight;
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
};
Text.prototype = Object.create(Font.prototype,{
	setLineHeight: {
		value: Text.prototype.setLineHeight
	}
});
let t = new Text(18, '#0f0', 16, true);
console.log( t ); // Text {color: "#0f0", size: 16, weight: true, lineHeight: 18}
t.setLineHeight(20);
console.log( t.lineHeight ); // 20

メソッドcreateの第二引数にオブジェクトを代入しています。このオブジェクトの中で新たにメソッドsetLineHeightを定義することで、このメソッドも生成されるオブジェクトに追加されます。メソッドcreateの詳しい使い方はMDNのページで確認してください。
以上で、問題はないように思えますが、マイナスポイントがあります。オブジェクトのプロパティconstructorの参照先は、次のような関係性が好ましい作りになります。

Aオブジェクト.prototype.constructor === Aオブジェクト
Aオブジェクトのインスタンス.constructor === Aオブジェクト

この関係性については、過去の記事「オブジェクトについて Vol.4(プロトタイプチェーン、コンストラクタ)」で確認してください。
それでは上のコードで、プロパティconstructorの参照先がどのようになっているか確認してみます。

sample.js
function Text(lineHeight=18,color='#000', size=14, weight=false){
	Font.call(this,color,size,weight);
	this.lineHeight = lineHeight;
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
};
Text.prototype = Object.create(Font.prototype,{
	setLineHeight: {
		value: Text.prototype.setLineHeight
	}
});
let t = new Text(18, '#0f0', 16, true);
console.log( t.constructor === Text ); // false
console.log( Text.prototype.constructor === Text ); // false
console.log( t.constructor ); // ƒ Font(color='#000', size=14, weight=false){...}
console.log( Text.prototype.constructor ); // ƒ Font(color='#000', size=14, weight=false){...}

上の結果を見ると、インスタンスのプロトタイプのコンストラクタ、TextオブジェクトのプロトタイプのコンストラクタがともにTextオブジェクトではなくなってしまいました。実際にコンストラクタを確認すると、Fontオブジェクトを参照しています。
これは、メソッドcreateを実行し、Textオブジェクトのプロトタイプに新たにFontオブジェクトを代入したためです。
これを正しくするために次のようにします。

sample.js
function Text(lineHeight=18,color='#000', size=14, weight=false){
	Font.call(this,color,size,weight);
	this.lineHeight = lineHeight;
};
Text.prototype.setLineHeight = function(value){
	this.lineHeight = value;
};
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
};
Text.prototype = Object.create(Font.prototype,{
	setLineHeight: {
		value: Text.prototype.setLineHeight
	}
});
Text.prototype.constructor = Text;
let t = new Text(18, '#0f0', 16, true);
console.log( t.constructor === Text ); // true
console.log( Text.prototype.constructor === Text ); // true
console.log( t.constructor ); // ƒ Text(lineHeight=18,color='#000', size=14, weight=false){...}
console.log( Text.prototype.constructor ); // ƒ Text(lineHeight=18,color='#000', size=14, weight=false){...}

上の赤くなっている1行を追加することで、問題が解決しました。メソッドcreateでプロトタイプを新たに生成した後に、プロトタイプのコンストラクタに再度Textオブジェクトを代入しました。これにより、Textオブジェクトのインスタンス、Textオブジェクトのそれぞれのプロトタイプのコンストラクタの参照先がTextオブジェクトになりました。
以上が、Objectオブジェクトのメソッドcreateを使用したプロパティの継承になります。