【JS】 クラスについてまとめる

【JS】 クラスについてまとめる

クラスとは

クラスはES6 から追加された新しくオブジェクトを作成するための雛形です。
ES5 までは「コンストラクタ関数」と「プロトタイプ」を用いてオブジェクトの雛形を作っていたのですが、それのシンタックスシュガーとして登場したのが「クラス」です。

ですのでクラス内部での処理は「コンストラクタ関数とプロトタイプ」と同様の処理が行われています。

クラスの定義

クラスの定義には以下の2つの方法があります。

  • クラス宣言
  • クラス式

クラス宣言

クラス内には必ずコンストラクタをconstructorという名前のメソッドとして定義します。
constructor内の処理はクラスをインスタンス化したときに自動的に呼び出されます。

class Person {
	constructor(name, age) {
		this.name = name;
		this.age = age;
	}

	greeting() {
		console.log(`こんにちは ${this.name}です`);
	}

	myAge() {
		console.log(`私は ${this.age}歳です`);
	}
}

const taro = new Person('太郎', 23);
taro.greeting()// => こんにちは 太郎です
taro.myAge()// => 私は 23歳です

コンストラクタの処理が不要なら省略することも可能です

class Person2 {
	// コンストラクタの処理が不要な場合は省略できる
}

クラス式

クラス式は、クラスを値として定義する方法で関数で言う関数式です。
また、クラス式ではクラス名を省略できます。

const Person = class {
	constructor(name, age) {
		this.name = name;
		this.age = age;
	}

	greeting() {
		console.log(`こんにちは ${this.name}です`);
	}

	myAge() {
		console.log(`私は ${this.age}歳です`);
	}
}

const taro = new Person('太郎', 23);
taro.greeting()// => こんにちは 太郎です
taro.myAge()// => 私は 23歳です

クラス宣言とクラス式の違い

2つとも構文的にはあまり差はありませんが、クラス式ではクラスを再宣言することができると言う特徴があります。

let Person = class {
	constructor(name, age) {
		this.name = name;
		this.age = age;
	}

	greeting() {
		console.log(`こんにちは ${this.name}です`);
	}

	myAge() {
		console.log(`私は ${this.age}歳です`);
	}
}

//クラス式では最宣言が可能
Person = class {
	constructor(name, age) {
		this.name = name;
		this.age = age;
	}

	greeting() {
		console.log(`こんにちは ${this.name}です(再宣言)`);
	}

	myAge() {
		console.log(`私は ${this.age}歳です(再宣言)`);
	}
}

const taro = new Person('太郎', 23);
taro.greeting()// => こんにちは 太郎です(再宣言)
taro.myAge()// => 私は 23歳です(再宣言)

また、クラス宣言とクラス式どちらも、メソッドは関数式では記述できないという特徴があります。
通常のオブジェクトのメソッドのように:による区切りがないためアロー関数や無名関数の書き方で関数を定義した場合シンタックスエラーになります。

クラスのインスタンス化

クラスからオブジェクトを生成する際、

  • オブジェクトを生成することをインスタンス化
  • 生成されたオブジェクトをインスタンス

と呼びます。

インスタンスの生成

インスタンス化をする際はnew演算子を使用します。

class Person {
}
// `Person`をインスタンス化する
const person = new Person();

// 新しくインスタンス(オブジェクト)を作成する
const newPerson = new Person();

それぞれのインスタンスは異なるオブジェクト

最初にインスタンスとして定義したpersonと新しくインスタンス化したnewPersonは異なるオブジェクトとして定義されます。また、それぞれがどのクラスのインスタンスかと言うのははinstanceof演算子を使って判定することができます。

class Person {
}
// `Person`をインスタンス化する
const person = new Person();

// 新しくインスタンス(オブジェクト)を作成する
const newPerson = new Person();

// それぞれのインスタンスは異なるオブジェクト
console.log(person === newPerson); // => false

// どのクラスのインスタンスか判定する
console.log(person instanceof Person); // => true
console.log(newPerson instanceof Person); // => true

各インスタンスはそれぞれ同じプロトタイプメソッドを共有している

各インスタンスは異なるオブジェクトですがそれぞれ同じメソッドを共有しています。(同じ関数を参照している)

このインスタンス間で共有されるメソッドのことをプロトタイプメソッドと呼びます。

class Person {
	constructor(name) {
		this.name = name;
	}

	greeting() {// プロトタイプメソッド
		console.log(`こんにちは ${this.name}です`);
	}

}
const person = new Person();
const newPerson = new Person();

// 各インスタンスオブジェクトのメソッドは共有されている(同じ関数を参照している)
console.log(person.greeting === newPerson.greeting);// => true

Personのプロトタイプオブジェクトに定義されたgreetingメソッドは各インスタンス間で参照を共有されています。

各インスタンスのプロパティはそれぞれ別の状態を持つ

各インスタンスはそれぞれ同じプロトタイプメソッドを共有していますがプロパティはそれぞれ別の状態を持ちます。

各インスタンスのプロパティの状態を見るためにCounterクラスを定義します。

class Counter {
	constructor() {
		this.num = 0;
	}

	increment() {
		this.num++;
	}
}
const counter = new Counter();
const newCounter = new Counter();
counter.increment();

//numプロパティはそれぞれ状態が異なる 
console.log(counter.num);// => 1
console.log(newCounter.num);// => 0

counterでのみincrementメソッドを呼び出した場合、
counter.numの値は1で、
newCounter.numの値は0
になっていることから各インスタンスでプロパティの状態が違うことがわかります。

アクセッサプロパティ(getter, setter)

クラスではメソッドを呼び出さずに使用できる特殊なメソッド「アクセッサプロパティ」が存在し、プロパティの参照(getter)、プロパティへの代入(setter)時に呼び出されます。

getter

getterは、簡単に言うと「オブジェクトのプロパティを参照してきた時に、自動で行われるメソッド」です。

getterはプロパティ名の前にgetをつけます。
また、getter(get)には必ず戻り値をつける必要があります。

class クラス名 {
    constructor(仮引数1, 仮引数2) {
        プロパティを記述
    }
    メソッド名(){
        メソッドの内の処理を記述
    }
    get プロパティ名() {
        プロパティが参照された時の動作を記述する

        return プロパティ;
    }
}

const インスタンス = new クラス名();
インスタンス.プロパティ名;// getterが呼び出される

実際に書いてみます。

class Person {
	constructor(name) {
		this._name = name;
	}
	greeting() {
		console.log(`こんにちは ${this._name}です`);
	}

	get name() {
		//nameプロパティが取得された時の動作
		console.log("自己紹介");
		return this._name;// プロパティを返す
	}
}

const taro = new Person(c);
console.log(taro.name);
// => 自己紹介
// => 太郎

このようにnameプロパティを取得するとgetter(get)を経由することになるので”太郎”の前に”自己紹介”と出力されます。

setter

getterは「オブジェクトのプロパティを参照してきた時に、自動で行われるメソッド」であるのに対して、

setterは「オブジェクトのプロパティを代入した時に、自動で行われるメソッド」となります。

setter(set)の仮引数にはプロパティへ代入する値が入ります。しかし、getterとは違って戻り値をつける必要はありません。

class クラス名 {
    constructor(仮引数1) {
        プロパティを記述する
    }
    メソッド名(){
        メソッドの内の処理を記述
    }
    set プロパティ名(仮引数)  {
        仮引数のプロパティが取得された時の動作を記述する
        this.プロパティ = 仮引数;
    }
}

const インスタンス = new クラス名();
インスタンス.プロパティ名 = 値;// setterが呼び出される

実際に書いてみます

class Person {
	constructor(name, age) {
		this._name = name;
		this._age = age;
	}
	greeting() {
		console.log(`こんにちは ${this._name}です`);
	}

	set age(newAge) {
		//ageプロパティに値が代入された時の動作
		console.log(`${this._name}が${newAge}歳になりました。誕生日おめでとう!`);
		this._age = newAge;
	}
}

const taro = new Person("太郎", 23);
console.log(taro);
taro.age = 24;
console.log(taro);
//▶︎ Person { _name: '太郎', _age: 23 }
// => 太郎が24歳になりました。誕生日おめでとう!
//▶︎ Person { _name: '太郎', _age: 24 }

このようにageプロパティに新しい値を代入するとsetter(set)を経由し新しい値を代入されていることがわかります。

静的メソッド(static)

staticとはクラス内でのみ使用することができるキーワードであり、
クラスをインスタンス化しなくても、使用が可能な静的なメソッドです。

静的メソッドの定義方法はメソッド名の前に、staticをつけるだけです。

class Person {
	constructor(name) {
		this._name = name;
	}

	static private() {
		console.log(`この文書は個人情報です。`);
	}
}

// インスタンス化を行わず呼び出し
Person.private();// => この文書は個人情報です。

クラス継承

クラスはextendsを使うことで他のクラスのプロパティやメソッドを継承することができます。
これをクラスの継承または、サブクラス化と呼びます。

クラスの継承はextendsで継承元となる親クラスを指定することで、 親クラスを継承した子クラスを定義できます。

定義した子クラスから親クラスを参照するにはsuperキーワードを利用します。

class 継承先のクラス名 extends 元のクラス名 {
    constructor(元のクラスの仮引数, 継承先のクラスに取る仮引数) {
        super(元のクラスの仮引数);

        追加したいプロパティを this.プロパティ名 = 仮引数;

    }

    追加、上書きメソッドを記述

}

実際に書いてみます。

class Person {
	constructor(name, age) {
		this.name = name;
		this.age = age;
	}

	greeting() {
		console.log(`こんにちは ${this.name}です`);
	}

	myAge() {
		console.log(`私は ${this.age}歳です`);
	}
}

// Personを継承する
class ChildPerson extends Person {
	constructor(name, age, gender) {
                // constructor内では必ずsuperを先頭に持ってくる
		super(name, age);// Personのコンストラクタ処理を呼び出す。
		this.gender = gender;
	}

	//greetingの内容を書き換え
	greeting() {
		console.log(`今晩わ ${this.name}です`);
	}

	// 新しいメソッドを追加
	myGender() {
		console.log(`私の性別は ${this.gender}です`);
	}
}

const taro = new ChildPerson('太郎', 23, '男');
console.log(taro);//▶︎ ChildPerson {name: '太郎', age: 23, gender: '男'}
taro.greeting();// => 今晩わ 太郎です
taro.myAge();// => 私は 23歳です
taro.myGender();// => 私の性別は 男です

継承先のChildPersonクラスでは、継承元のクラスPersonのgreetingメソッド内の処理を書き換え、新たにmyGenderメソッドを追加しました。
また、extendsキーワードはプロパティだけでなく、メソッドも継承するため、元のクラスにのみ書かれているmyAgeメソッドも問題なく使えるようになっています。

また、注意点として 子クラスのコンストラクタ 内では、thisを参照する前にsuper()で親クラスのコンストラクタ処理を呼び出さないとエラーとなります。

チェーンメソッド

1つのインスタンスに対して連続してメソッドを呼び出す場合チェーンメソッドという方法があります。
チェーンメソッドを利用する場合、メソッドの返り値にインスタンスを設定する必要があります。

class Person {
	constructor(name, age, gender, birthday) {
		this.name = name;
		this.age = age;
		this.gender = gender;
		this.birthday = birthday;
	}
	greeting() {
		console.log(`こんにちは ${this.name}です`);
		return this;
	}

	myAge() {
		console.log(`私は ${this.age}歳です`);
		return this;
	}
	
	myGender() {
		console.log(`私の性別は ${this.gender}です`);
		return this;
	}

	myBirthday() {
		console.log(`私の誕生日は ${this.birthday} です`);
		return this;
	}
}

const taro = new Person('太郎', 23, '男', '1997/6/20');
taro.greeting().myAge().myGender().myBirthday();
// => 今晩わ 太郎です
// => 私は 23歳です
// => 私の性別は 男です
// => 私の誕生日は 1997/6/20 です

メソッド内のthisの参照先がインスタンス化されたオブジェクト(インスタンス)であることから、return this;とすることでメソッドの返り値にインスタンスを設定することができます。

この場合のthisはtaroオブジェクトを参照します。

まとめ: クラスのメリット

  • 何度も同じコードを書く必要がなく、効率的にプログラムが組める
  • クラスはコンストラクタ関数とプロトタイプのシンタックスシュガーであるため、メモリを効率的に使用できる
  • クラス内で処理を細かく分けることができるため、処理の流れが追いやすく、保守・メンテナンスが容易

参考文献