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

【JS入門】オブジェクトの作り方 Vol.3(class定義)

2018年2月1日 / category : javascript

オブジェクトの作り方の最後はES6から追加された機能「クラス定義」による作り方を解説します。オブジェクト指向というと、クラスベースとプロトタイプベースの大きく2つに分けられますが、JavaScriptはプロトタイプベースです。クラス定義(クラス式・クラス宣言)が追加されましたが、オブジェクトを作成し、プロパティを継承するための構文が用意されただけであって、新しくクラスベースのオブジェクト指向のモデルが追加されたわけではありません。
まずは、クラス定義の構文ですが「クラス式」と「クラス宣言」の2つがあります。

クラス定義によるオブジェクト生成

クラス式

let 変数 = class{ クラス文 }; //名前なし
let 変数 = class クラス名{ クラス文 }; //名前あり

クラス宣言

class クラス名{ クラス文 };

クラス式

まずはクラス式を使用した簡単な構文を見てみます。

sample.js
let Test = class{};
let t = new Test();
console.log( t ); // Test {}

変数Testにクラス式を代入しています。インスタンス(オブジェクト)を生成する場合は、new+コンストラクタ関数()です。
class式で返ってくる値(データ)がコンストラクタ関数になるため変数Testがそのまま関数名になる仕組みです。
その仕組みは次のコードで確認できます。
生成された変数tを見ると、Textオブジェクトのインスタンスが生成されていることがわかります。
class定義されたデータがどんなデータか実際に確かめてみます。

sample.js
let Test = class{};
console.log( Test ); // class {}
console.log( typeof Test ); // function
console.log( Test instanceof Object ); // true
console.log( Test instanceof Function ); // true
console.log( Test.constructor === Function ); // true
console.log( Test.__proto__ === Function.prototype ); // true
console.log( Test.prototype ); // {constructor: ƒ}

上の結果を順番に見てみます。変数Testには、class式が代入されていますが、このclass式の型はfunction(Object型)ということがわかり、コンストラクタ関数として認識されます。class式が代入された変数Testを便宜上Testクラスと呼ぶことにします。Testクラスは、Functionオブジェクトのインスタンスですので、プロパティconstructorにはFunctionオブジェクトを参照し、プロトタイプ(プロパティ__proto__)は、Functionオブジェクトのプロトタイプを参照しています。

クラス宣言

sample.js
class Test{};
let t = new Test();
console.log( t ); // Test {}

クラス宣言もクラス式と同様に、new+コンストラクタ関数()でインスタンス(オブジェクト)を生成します。
仕組みについても、クラス式と同様です。
new+コンストラクタ関数()でインスタンス(オブジェクト)を生成するという仕組みについては、関数定義によるオブジェクト生成と変わりません。では、クラス文を見てみます。

コンストラクタ(constructor)

sample.js
class Test{
	constructor(_name, _age){
		this.name = _name;
		this.age = _age;
		console.log( this.name, this.age );  //'吉本' 37
	}
};
let t = new Test('吉本', 37);

クラス定義の場合は、constructorというメソッドでコンストラクタ関数の関数文を設定します。関数定義によるオブジェクト生成する場合は、その関数自身がコンストラクタ関数の関数文となっていました。

sample.js
//クラス定義でのオブジェクト生成
class Test{
	constructor(){
		//コンストラクタ関数の関数文
		console.log('コンストラクタ関数');
	}
};
let t = new Test();

//関数定義でのオブジェクト生成
function Test2(){
	//コンストラクタ関数の関数文
	console.log('コンストラクタ関数');
}
let t2 = new Test2();

メソッドconstructorは、オブジェクト生成時に実行されます。クラスに1つしか定義できません。複数定義してしまうとエラーになります。続いて、メソッドの定義ですが、クラス定義の場合、メソッドが2種類あります。

プロトタイプメソッドとスタティックメソッド(静的メソッド)

sample.js
class Test{
	message(){
		console.log('プロトタイプメソッドです。');
	}
};
let t = new Test();
t.message();

クラス定義内にあるmessageがプロトタイプメソッドになります。このメソッドはインスタンスからアクセスができます。

sample.js
class Test{
	static message(){
		console.log('スタティックメソッドです。');
	}
};
Test.message();

メソッド名の前にstaticを付けるとスタティックメソッドになります。このメソッドはインスタンスからはアクセスできません。クラスが直接アクセスするメソッドになります。

クラス定義ではgetter/setterが使える

sample.js
class Test{
	constructor(_text){
		this.text = _text;
	}
	set message(_text){
		this.text = _text;
	}
	get message(){
		return this.text;
	}
};
let t = new Test('Testクラスです。');
console.log( t.message );
t.message = 'getter/setterが使えます。';
console.log( t.message );

getter/setterについては、過去の記事「オブジェクトの作り方 Vol.1(オブジェクト初期化子)」で触れています。この機能がクラス定義内で使用可能です。

クラス定義でのthis

まずは関数定義によるオブジェクト生成で、次のようなコードを見てみます。

sample.js
function Test(){}
Test.prototype.a = function(){
	return this;
};
Test.b = function(){
	return this;
};

let t = new Test();
console.log( t.a() );  //Test {}
console.log( Test.b() );  //ƒ Test(){}

let A = t.a;
console.log( A() );  //Window {...}

let B = Test.b;
console.log( B() );  //Window {...}

上の結果は想像できたでしょうか。コンソールの最初の2つは、それぞれthisがインスタンスとTestオブジェクトを参照しています。thisについては過去の記事「コンストラクタ関数について」で触れていますが、thisについては改めて記事にしたいと思います。
プロトタイプに定義したメソッドaと、オブジェクトに定義したメソッドbをそれぞれ変数A、変数Bに代入し、実行すると参照するthisが変わります。これは代入された関数文を見ると想像できるかと思います。メソッドa、メソッドbともに次の無名関数が代入されています。

メソッドa、メソッドbに代入されている関数

function(){
	return this;
};

この関数を変数A、変数Bに代入し、実行

let A = function(){
	return this;
};
console.log( A() );

let B = function(){
	return this;
};
console.log( B() );

実行された場所がトップレベル(グローバル)のため、Windowオブジェクトが返ってきています。
これと同じことをクラス定義で試してみます。

sample.js
class Test{
	a(){
		return this;
	}
	static b(){
		return this;
	}
};
let t = new Test();
console.log( t.a() );  //Test {}
console.log( Test.b() );  //class Test{}

let A = t.a;
console.log( A() );  //undefined

let B = Test.b;
console.log( B() );  //undefined

コンソールの最初の2つまでは結果が同じですが、変数に代入したメソッド内のthisの参照結果がundefinedになりました。
クラス定義の場合、thisにオブジェクトが紐づいていない状態で実行された場合、undefinedになります。

クラス定義のプロパティの継承(拡張によるサブクラス化)

関数定義によるプロパティの継承は、前回の記事「 オブジェクトの作り方 Vol.2(コンストラクタ関数・プロパティの継承)」で、ObjectオブジェクトのメソッドsetPrototypeOfまたは、メソッドcreateを使用する方法を解説しました。
クラス定義の場合は、extendsキーワードを使用しますが、プロパティの継承という言い方よりクラスを拡張する、という言い方のほうが良いかもしれません。

class クラス名 extends クラス名{ クラス文 };

構文は上のようになります。次のコードを見てみます。

sample.js
class Font{
	constructor(color='#000', size=14, weight=false){
		this.color = color;
		this.size = size;
		this.weight = ( weight ) ? true : false;
	}
	setColor(value){
		this.color = value;
	}
}

class Text extends Font{
	constructor(lineHeight=18, color='#000', size=14, weight=false){
		super(color,size,weight);
		this.lineHeight = lineHeight;
	}
	setLineHeight(value){
		this.lineHeight = value;
	}
}

let t = new Text();
console.log( t );  //Text {color: "#000", size: 14, weight: false, lineHeight: 18}

t.setLineHeight(20);
console.log( t.lineHeight );  //20

t.setColor('#f00');
console.log( t.color );  //#f00

上のコードを見ると、TextクラスにFontクラスを拡張し、Fontクラスで定義しているメソッドsetColorを使用できていることがわかります。拡張されるクラス(Fontクラス)のコンストラクタを実行する際には、superを使用します。(superについては後ほど、触れます。)
関数定義のオブジェクを拡張することもできます。

sample.js
function Font(color='#000', size=14, weight=false){
	this.color = color;
	this.size = size;
	this.weight = ( weight ) ? true : false;
}
Font.prototype.setColor = function(value){
	this.color = value;
}

class Text extends Font{
	constructor(lineHeight=18, color='#000', size=14, weight=false){
		super(color,size,weight);
		this.lineHeight = lineHeight;
	}
	setLineHeight(value){
		this.lineHeight = value;
	}
}

let t = new Text();
console.log( t );  //Text {color: "#000", size: 14, weight: false, lineHeight: 18}

t.setLineHeight(20);
console.log( t.lineHeight );  //20

t.setColor('#f00');
console.log( t.color );  //#f00

superについて

superを使用することで、親クラスを参照することができます。上のコードでは、コンストラクタが実行されたタイミングで、親クラスのコンストラクを実行するためにsuperを使用しました。

sample.js
class Font{
	constructor(color='#000', size=14, weight=false){
		this.color = color;
		this.size = size;
		this.weight = ( weight ) ? true : false;
	}
}

class Text extends Font{
	constructor(lineHeight=18, color='#000', size=14, weight=false){
		super(color,size,weight);
		this.lineHeight = lineHeight;
	}
}

superの引数は、親クラスのコンストラクタで必要な引数を代入します。Fontクラスのコンストラクタでは、color, size, weightの3つの引数が必要なため、Textクラスのコンストラク内でのsuperでは、3つの引数を設定し実行しています。次のコードを見てみます。

sample.js
class Font{
	constructor(){
		this.size = 14;
	}
	getSize(){
		return this.size;
	}
}

class Text extends Font{
	constructor(){
		super();
		this.lineHeight = 20;
	}
	getLineHeight(){
		return this.lineHeight;
	}
	getMessage(){
		return '行間は' + this.getLineHeight() + 'pxで、フォントサイズは' + super.getSize() + 'pxです。';
	}
}

let t = new Text();
console.log( t.getMessage() );  //行間は20pxで、フォントサイズは14pxです。

superをコンストラクタ以外のところで使用しています。superは親クラスを参照していますので、super.getSizeは、親クラスであるFontクラスのメソッドgetSizeを指しています。
以上が、クラス定義によるオブジェクト生成になります。