はじめに
JavaScriptにおいてのクラス構文は、ECMAScript2015(ES6)で追加された機能です。従来のプロトタイプベースのオブジェクト指向プログラミングに変わり、クラス構文によりわかりやすいコーディングを行うことができます。
クラス構文の理解はJavaScriptのフレームワークやライブラリなどを使用する上で必要になってくるので、ウェブアプリ開発などでは特に重要です。また、一般的なウェブサイト制作においても、昨今ではNext.jsやAstroといったフレームワークを使用することが増えています。これらを活かすためにもクラス構文の知識は有益です。
この記事では、JavaScript初学者の方に向けてクラスベースでのオブジェクト指向プログラミングの基本について触れ、クラス構文の基本的な使い方について解説します。
この記事の対象者
- JavaScript初学者の方
- クラス構文を学びたい方
- JavaScriptフレームワーク等に興味のある方
この記事で学べること
- クラス/インスタンスとは何か
- クラスの定義とインスタンスの生成方法について
- インスタンスメソッドとクラスメソッドについて
クラスベースの基本
クラスはオブジェクト指向プログラミングにおいて、プロパティやメソッドを共通化したオブジェクトを生成するための仕組みです。クラスは「オブジェクトの設計図」と考えることができます。また、クラスを使用して生成されたオブジェクトを「インスタンス」といいます。この用語はオブジェクト指向プログラミングでは頻繁に使われるので、基本として覚えておく必要があります。
クラスとインスタンスは、クラスベースのオブジェクト指向プログラミングの基本的な概念です。クラスは抽象的なものであり、インスタンスは具体的なデータや振る舞いを持つオブジェクトの実体という役割を持ちます。
どうしてクラスが必要なのか
クラスはオブジェクトの設計図だと前述しました。つまり、根本的には「オブジェクトを定義する」と同義であり、クラス構文を利用しなくてもオブジェクトリテラルで表現できる場合があります。
しかし、同じ構造を持つオブジェクトが複数必要になる場合、それらを全てオブジェクトリテラルで表現することは冗長です。何らかの値を変数として管理することで効率の良い開発ができるように、何らかのオブジェクトをクラスとして管理することも同様に作用します。
簡単な具体例として、クラスを使用せずに同じ構造を持つオブジェクトをリテラル表現で記述してみます。
const myCat1 = {
name: 'Chiro',
age: 1,
}
const myCat2 = {
name: 'Ohagi',
age: 1,
}
const myCat3 = {
name: 'Tofu',
age: 0,
}
同じ構造を持つオブジェクトが複数必要になる場合、一般的には各オブジェクトの利用目的は同じだと思います。つまり、オブジェクトの構造の変更が必要になった場合、関連する全てのオブジェクトを修正する必要があります。以下はawnerプロパティを追加する例です。
// 全てのオブジェクトに追加する必要があります
const myCat1 = {
name: 'Chiro',
age: 1,
awner: 'Naossan', // 追加
}
const myCat2 = {
name: 'Ohagi',
age: 1,
awner: 'Naossan', // 追加
}
const myCat3 = {
name: 'Tofu',
age: 0,
awner: 'Naossan', // 追加
}
クラスとして管理することで、修正箇所をクラスの定義とインスタンスの生成部分に限定することができ、保守性の向上に貢献します。
クラス構文の基本的な使い方
ここでは、クラス構文の基本的な使い方について解説します。クラス構文の基本的な使い方として、以下の方法について解説します。
- クラス定義とインスタンスの生成
- メソッドの定義
まずはクラス定義とインスタンスの生成です。
クラス定義とインスタンスの生成
クラスを定義するにはclassキーワードを使用します。クラス名は慣習的に先頭の文字を大文字にします。
// クラスの定義
class Cat {
// プロパティやメソッドを定義します
}
クラスはオブジェクトの設計図なので、インスタンスに設定するプロパティを定義することができます。クラス構文ではconstructor()メソッドを定義します。
class Cat {
// コンストラクタの定義
constructor(name, age) {
this.name = name;
this.age = age;
}
}
constructor()メソッドは、インスタンスを生成する際に自動で一度だけ呼び出される特別なメソッドです。スペルミスをすると機能しないので注意しましょう。
constructor()メソッドは、主にプロパティの初期化など、インスタンス生成時の初期設定を行います。constructor()メソッド内のthisは生成されるインスタンスを指しており、上記のような記述でインスタンスにプロパティを設定できます。
インスタンスの生成はnewキーワードを使用します。
class Cat {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// インスタンスの生成
const myCat = new Cat('Chiro', 1);
console.log(myCat);
/*
結果:Cat {
name: 'Chiro',
age: 1
}
*/
インスタンス生成時の引数の値がconstructor()メソッドに渡り、myCatのプロパティの初期値として設定されます。デフォルト値や固定値を設定する場合は以下のように記述することができます。
class Cat {
// awnerにデフォルト値を設定
constructor(name, age, awner = 'Nora') {
this.name = name;
this.age = age;
this.awner = awner;
// 固定値を設定
this.animalType = 'cat';
}
}
const noraCat = new Cat();
console.log(noraCat);
/*
結果:Cat {
name: undefined,
age: undefined,
awner: 'Nora',
animalType: 'cat'
}
*/
上記のように、constructor()メソッドにもES6で追加された関数のデフォルト値の設定方法が使用できます。awnerの部分です。
// awnerにデフォルト値を設定
constructor(name, age, awner = 'Nora') {
…
}
また、プロパティに設定する値は必ず引数として受け取った値である必要はありません。animalTypeプロパティの値はCatクラスで生成された全てのインスタンスに同じ値が設定されます。
constructor(name, age, awner = 'Nora') {
…
// 固定値を設定
this.animalType = 'cat';
}
メソッドの定義
メソッドの定義方法の前に、メソッドとは何かについて簡単に説明します。
メソッドは「オブジェクトのふるまい」と表現できます。Catクラスを例にして考えてみると、Catクラスの意義はたくさんのネコを生成するためのクラスです。
ネコは鳴く、遊ぶ、甘える、ご飯を食べる、寝る、寝る、といった行動をとり、性格などによってふるまいは違っても、基本的にはどのようなネコもこれらのような行動を持ちます。オブジェクト指向においてのメソッドは、この鳴いたり遊んだりとった行動を定義するためのものです。
クラス構文では、メソッドは「インスタンスメソッド」と「クラスメソッド」を定義することができます。これらの特徴は以下です。
- インスタンスメソッド:インスタンスでしか呼び出せないメソッド
- クラスメソッド:インスタンスを生成せずに呼び出せるメソッド
メソッドの基本として、まずはインスタンスメソッドの例を挙げます。
インスタンスメソッド
インスタンスメソッドの扱いは、オブジェクトリテラルでのメソッドの定義とほぼ同じです。以下はインスタンスメソッドの簡単な例です。
class Cat {
constructor(name, age) {
this.name = name;
this.age = age;
}
// インスタンスメソッド
meow() {
console.log('Nyan!');
}
}
インスタンスメソッドを呼び出すには、まずインスタンスを生成する必要があります。生成されたインスタンスから、通常のオブジェクトと同様にドットシンタックスでインスタンスメソッドを呼び出すことができます。
const myCat = new Cat('Chiro', 1);
// インスタンスメソッドの呼び出し
myCat.meow();
// 結果:Nyan!
インスタンスメソッド内のthisはconstructor()メソッドと同様に生成されたインスタンスを指します。なので、インスタンスの生成時に渡された引数はconstructor()を介して使用することができます。例として、「上手に鳴けないネコ」を表現してみます。
class Cat {
constructor(name, age, voice = 'Nyan!') {
this.name = name;
this.age = age;
this.voice = voice;
}
// インスタンスメソッド
meow() {
console.log(this.voice);
}
}
const myCat = new Cat('Ohagi', 1, '...shhhn...');
myCat.meow();
// 結果:...shhhn...
変更点を見てみましょう。constructior()メソッドにvoiceを追加しており、デフォルト値は「’Nyan!’」です。インスタンスの生成時に第三引数を記述することで、独自の鳴き声を定義できるようになります。
constructor(name, age, voice = 'Nyan!') {
…
this.voice = voice;
}
インスタンスメソッドであるmeow()の内容も少し変わっています。
meow() {
// 引数はインスタンスのvoiceプロパティの値になります
console.log(this.voice);
}
続いてクラスメソッドについて見ていきます。
クラスメソッド
前述のとおり、クラスメソッドはインスタンスを生成せずに呼び出すことができます。そして、インスタンス側からドットシンタックスでクラスメソッドを呼び出すことはできません。Array.from()のようなコンストラクタ関数に直接紐づいたメソッドと同じで、呼び出し方も同じです。
クラスメソッドはクラス全体に対する操作を行う際に利用できます。例として、Catクラスで生成されたネコインスタンスの一覧をコンソールに出力するメソッドを定義してみます。
class Cat {
constructor(name, age) {
this.name = name;
this.age = age;
}
/*
ネコインスタンスの一覧を
配列として受け取ります。
*/
static showCats(catList) {
catList.forEach((cat, index) => {
console.log(`${index + 1}. ${cat.name}, Age: ${cat.age}`);
});
}
}
クラスメソッドはメソッド名の前にstaticキーワードを記述します。staticは「静的」という意味で、クラスメソッドは静的メソッドと呼ばれることもあります。staticキーワードにより、インスタンスではなくクラス自体に関連付けられたメソッドを定義できます。上記のshowCats()メソッドを呼び出すには以下のように記述します。
// インスタンスの生成
const myCat1 = new Cat('Chiro', 1);
const myCat2 = new Cat('Ohagi', 1);
const myCat3 = new Cat('Tofu', 0);
// showCats()に渡す配列
const myCatList = [myCat1, myCat2, myCat3];
// showCats()の呼び出し
Cat.showCats(myCatList);
/*
結果:
1. Chiro, Age: 1
2. Ohagi, Age: 1
3. Tofu, Age: 0
*/
上記の例ではインスタンス一覧の配列をハードコードしていますが、インスタンスが増えるたびにmyCatListに記述していくのは手間です。ネコインスタンスを保存するためのクラスプロパティを定義して、インスタンスの生成時に配列に自動で追加するような処理を実装してみます。
class Cat {
// クラスプロパティ
static catList = [];
constructor(name, age) {
this.name = name;
this.age = age;
Cat.addCatList(this);
}
// ネコ一覧をコンソールに表示するクラスメソッド
static showCats(catList = Cat.catList) {
catList.forEach((cat, index) => {
console.log(`${index + 1}. ${cat.name}, Age: ${cat.age}`);
});
}
// ネコインスタンスをCat.catListに追加するメソッド
static addCatList(cat) {
Cat.catList.push(cat);
}
}
const myCat1 = Cat.createCat('Chiro', 1);
const myCat2 = Cat.createCat('Ohagi', 1);
const myCat3 = Cat.createCat('Tofu', 0);
Cat.showAllCats();
/*
結果:
1. Chiro, Age: 1
2. Ohagi, Age: 1
3. Tofu, Age: 0
*/
変更点を見ていきます。まずは以下の部分です。
class Cat {
// クラスプロパティ
static catList = [];
…
// 引数のデフォルト値をcatListにします
static showCats(catList = Cat.catList) {
…
}
…
}
staticキーワードはプロパティにも使用でき、これをクラスプロパティ(静的プロパティ)と言います。クラスプロパティはクラス自体に関連付けられたプロパティであり、インスタンス毎ではなくクラス全体で共有されます。
ここではcatListという名前の空の配列を定義しており、前述のサンプルコードのmyCatListに相当する役割を持ちます。このcatListをshowCats()メソッドに渡しています。
showCatsメソッドの引数も少し変わっています。「catList = Cat.catList」と記述していますが、staticキーワードを使用している場合はthisがインスタンスではなくクラスになるので、「catList = this.catList」でも動作します。ここでは、Cat.catListの方が明示的で分かりやすいのでこちらを選択しています。
インスタンスの生成時にcatListにそのインスタンスを追加すれば、前述の例のようにインスタンスが増える度に手動で追加するような作業はいらなくなります。以下の部分では、addCatList()というクラスメソッドを定義してインスタンスをcatListに追加する処理を記述しています。
// ネコインスタンスをCat.catListに追加するメソッド
static addCatList(cat) {
Cat.catList.push(cat);
}
addCatList()メソッドはどこで使用することができるでしょうか。インスタンスの生成時に呼び出されるconstructor()という特別なメソッドがありました。このメソッドは一度しか実行されないので、ここでaddCatList()を呼び出すと良さそうです。
constructor(name, age) {
this.name = name;
this.age = age;
Cat.addCatList(this);
}
これで、インスタンスが生成されたタイミングでaddCatList()が実行されます。constructor()内のthisは生成されるインスタンスなので、これによってインスタンスがcatListに追加されることになります。
const myCat1 = new Cat('Chiro', 1);
const myCat2 = new Cat('Ohagi', 1);
const myCat3 = new Cat('Tofu', 0);
Cat.showAllCats();
/*
結果:
1. Chiro, Age: 1
2. Ohagi, Age: 1
3. Tofu, Age: 0
*/
ちなみに、showAllCats()メソッドの引数はあくまでデフォルト値が「Cat.catList」なだけなので、メソッドの引数の配列をfilter()メソッドで絞り込んで出力することもできます。
// 1才のネコ一覧を表示
Cat.showCats(
Cat.catList.filter(
cat => cat.age === 1
)
);
/*
結果:
1. Chiro, Age: 1
2. Ohagi, Age: 1
*/
最後に
この記事では、JavaScript初学者の方に向けてクラスベースでのオブジェクト指向プログラミングの基本について触れ、クラス構文の基本的な使い方について解説しました。
オブジェクト指向プログラミングでは、クラスを管理するための機能として他にも「クラスの継承」や「Getter/Setter」というものがあります。少し複雑にはなっていきますが、これらの機能を使いこなせるとクラス管理やプロパティのエラーハンドリングなどを効率良く行うことができます。
次の記事では、クラスの継承について解説する予定です。クラスの継承は、クラスを構造化して管理しやすくするための機能です。クラスを構造化することで、コードの再利用性や保守性、拡張性を向上させることができます。