1. はじめに
JavaScriptの基礎を学んだ後、もう少し実践的な内容に踏み込みたいと思い、クラスについて学習しました。
これまで扱ってきた数値や文字列、配列といった基本的なデータ型に加えて、アプリケーション独自のデータ型を作る仕組みとして、クラスは重要な概念のようです。
学習教材の動画を見ながら実際にコードを書いて試したので、その内容を整理していきます。
2. クラスの基本
クラスは、独自のデータ型を作るための仕組みです。
この章では、クラスの基本的な書き方とインスタンスの作成方法について見ていきます。
2.1 クラスの定義とインスタンス生成
例えば、試験の点数を管理するアプリケーションを作る場合、名前と点数をまとめて扱える「User型」のようなデータ型を自分で定義できます。
class User {
}
const user1 = new User();
const user2 = new User();
console.log(user1); // User {}
console.log(user2); // User {}
new User()とすることで、Userクラスのインスタンスが作られます。この時点では中身は空ですが、これから値を持たせていきます。
クラスで定義した独自のデータ型から作られる値を「インスタンス」、そのデータ型に特化した独自の命令を「メソッド」と呼びます。
2.2 プロパティの設定
インスタンスに名前や点数といったデータを持たせるには、プロパティを設定します。
class User {
}
const user1 = new User();
user1.name = "Taro";
user1.score = 70;
const user2 = new User();
user2.name = "Jiro";
user2.score = 80;
console.log(user1.name); // Taro
console.log(user1.score); // 70
console.log(user2.name); // Jiro
console.log(user2.score); // 80
.nameや.scoreのように、ドット記法で値を設定・取得できます。これらをプロパティと呼びます。
3. constructor(コンストラクタ)
コンストラクタは、インスタンス作成時に自動的に実行される特別なメソッドです。
毎回インスタンスを作った後に個別にプロパティを設定する手間を省けます。
class User {
constructor(name, score) {
this.name = name;
this.score = score;
}
}
const user1 = new User("Taro", 70);
const user2 = new User("Jiro", 80);
console.log(user1.name); // Taro
console.log(user1.score); // 70
console.log(user2.name); // Jiro
console.log(user2.score); // 80
new User("Taro", 70)のように値を渡すと、constructorが自動的に呼ばれて、インスタンスのプロパティに値がセットされます。
thisは「このクラスから作られるインスタンス」を指す特殊なキーワードです。
4. メソッドの定義
メソッドは、そのクラスに特化した処理を定義できる機能です。
データを加工したり、情報を整形して返すような処理をまとめられます。
class User {
constructor(name, score) {
this.name = name;
this.score = score;
}
getUserString() {
return `${this.name}: ${this.score}点`;
}
}
const user1 = new User("Taro", 70);
const user2 = new User("Jiro", 80);
console.log(user1.getUserString()); // Taro: 70点
console.log(user2.getUserString()); // Jiro: 80点
getUserString()メソッドを定義することで、名前と点数を1行でまとめて表示できるようになりました。メソッドを呼び出すには、インスタンスに.getUserString()のようにドットでつなぎます。
クラス内でメソッドを定義する際、functionキーワードは不要です。
5. クラスの組み合わせ
複数のクラスを連携させることで、より柔軟で保守性の高い設計ができます。
ここでは、Scoreクラスを新たに作成して、Userクラスと組み合わせてみます。
5.1 Scoreクラスの導入
より実践的な例として、点数に科目の情報も含めたいケースを考えます。
class Score {
constructor(subject, result) {
this.subject = subject;
this.result = result;
}
getScoreString() {
return `${this.subject}: ${this.result}点`;
}
}
class User {
constructor(name, score) {
this.name = name;
this.score = score;
}
getUserString() {
return `${this.name} - ${this.score.getScoreString()}`;
}
}
const user1 = new User("Taro", new Score("数学", 70));
const user2 = new User("Jiro", new Score("英語", 80));
console.log(user1.getUserString()); // Taro - 数学: 70点
console.log(user2.getUserString()); // Jiro - 英語: 80点
Userクラスのscoreプロパティに、Scoreクラスのインスタンスを渡すことで、科目と点数を一緒に管理できるようになりました。
5.2 判定(grade)の追加
点数に応じてA判定、B判定といった評価を表示したい場合、Score側に処理を追加します。
class Score {
constructor(subject, result) {
this.subject = subject;
this.result = result;
}
getScoreString() {
let grade;
if (this.result >= 80) {
grade = "A";
} else {
grade = "B";
}
return `${this.subject}: ${this.result}点 (${grade}判定)`;
}
}
class User {
constructor(name, score) {
this.name = name;
this.score = score;
}
getUserString() {
return `${this.name} - ${this.score.getScoreString()}`;
}
}
const user1 = new User("Taro", new Score("数学", 70));
const user2 = new User("Jiro", new Score("英語", 80));
console.log(user1.getUserString()); // Taro - 数学: 70点 (B判定)
console.log(user2.getUserString()); // Jiro - 英語: 80点 (A判定)
ScoreクラスのgetScoreString()内で判定処理を行うことで、Userクラス側を変更せずに機能を追加できました。
grade変数にthisがつかないのは、メソッド内だけで使う一時的な変数だからです。一方、this.subjectやthis.resultは、複数のメソッドで使われるプロパティなのでthisをつけます。
5.3 getGradeメソッドと条件演算子
判定処理を別メソッドに切り出して、さらに見通しをよくします。
class Score {
constructor(subject, result) {
this.subject = subject;
this.result = result;
}
getGrade() {
return this.result >= 80 ? "A" : "B";
}
getScoreString() {
const grade = this.getGrade(); // 判定メソッドを呼び出し
return `${this.subject}: ${this.result}点 (${grade}判定)`;
}
}
class User {
constructor(name, score) {
this.name = name;
this.score = score;
}
getUserString() {
return `${this.name} - ${this.score.getScoreString()}`;
}
}
const user1 = new User("Taro", new Score("数学", 70));
const user2 = new User("Jiro", new Score("英語", 80));
console.log(user1.getUserString()); // Taro - 数学: 70点 (B判定)
console.log(user2.getUserString()); // Jiro - 英語: 80点 (A判定)
getGrade()メソッドを作ることで、判定ロジックが独立しました。条件演算子? :を使うと、if文よりもシンプルに書けます。
同じクラス内のメソッドを呼ぶ場合は、this.getGrade()のようにthisを付ける必要があります。
6. 継承(extends / super)
継承は、既存のクラスの機能を引き継いで新しいクラスを作る仕組みです。
科目ごとに判定基準を変えたい場合など、共通部分を再利用しながら一部だけカスタマイズできます。
class Score {
constructor(subject, result) {
this.subject = subject;
this.result = result;
}
getGrade() {
return this.result >= 80 ? "A" : "B";
}
getScoreString() {
const grade = this.getGrade();
return `${this.subject}: ${this.result}点 (${grade}判定)`;
}
}
// 数学用のクラス(判定基準を変更)
class MathScore extends Score {
constructor(result) {
super("数学", result); // 親クラスのconstructorを呼び出し
}
getGrade() {
return this.result >= 50 ? "A" : "B"; // 数学は50点以上でA
}
}
// 英語用のクラス(親の判定基準をそのまま使用)
class EnglishScore extends Score {
constructor(result) {
super("英語", result);
}
}
class User {
constructor(name, score) {
this.name = name;
this.score = score;
}
getUserString() {
return `${this.name} - ${this.score.getScoreString()}`;
}
}
const user1 = new User("Taro", new MathScore(70));
const user2 = new User("Jiro", new EnglishScore(80));
console.log(user1.getUserString()); // Taro - 数学: 70点 (A判定)
console.log(user2.getUserString()); // Jiro - 英語: 80点 (A判定)
extends Scoreとすることで、Scoreクラスの機能を引き継いだ子クラスを作れます。子クラスのconstructorでは、super()を使って親クラスのconstructorを呼び出します。
MathScoreクラスでは、getGrade()メソッドを再定義(オーバーライド)して、数学独自の判定基準(50点以上でA判定)に変更しました。EnglishScoreクラスは親クラスのロジックをそのまま使うので、getGrade()の再定義は不要です。
親クラスと同じ名前のメソッドを子クラスで定義することを「メソッドのオーバーライド」と呼びます。
7. フィールド(クラスのプロパティ一覧)
フィールドは、クラスの先頭にプロパティの一覧を明示的に書く機能です。
コードの可読性を高め、クラスの構造を一目で把握できるようにします。
class User {
name; // フィールド宣言
score; // フィールド宣言
constructor(name, score) {
this.name = name;
this.score = score;
}
}
const user1 = new User("Taro", 70);
console.log(user1.name); // Taro
console.log(user1.score); // 70
constructorと重複しているように見えますが、フィールドを書くことで以下のメリットがあります。
- クラスがどんなプロパティを持つか一目で分かる
- 他の人がコードを読む際に理解しやすくなる
特にチーム開発では、クラスの構造を明示することが重要になるようです。
8. static(静的プロパティ・静的メソッド)
静的プロパティ・メソッドは、クラス全体で共有される値や処理を定義する機能です。
インスタンスごとではなく、クラス自体に紐づくため、インスタンスの数をカウントするような処理に向いています。
8.1 静的プロパティ
インスタンスではなく、クラス全体で共有したい値がある場合、staticを使います。
class User {
static count = 0; // 静的プロパティ
name;
score;
constructor(name, score) {
this.name = name;
this.score = score;
User.count++; // インスタンス作成のたびにカウントアップ
}
}
const user1 = new User("Taro", 70);
const user2 = new User("Jiro", 80);
console.log(User.count); // 2
static count = 0;とすることで、クラス全体で共有されるプロパティが作られます。インスタンスが作られるたびにUser.count++でカウントアップすることで、何個のインスタンスが作られたかを追跡できます。
8.2 静的メソッド
クラス全体に紐づくメソッドも、staticを使って定義できます。
class User {
static count = 0;
name;
score;
constructor(name, score) {
this.name = name;
this.score = score;
User.count++;
}
static getUserCountString() {
return `${User.count} instance(s) created.`;
}
}
const user1 = new User("Taro", 70);
const user2 = new User("Jiro", 80);
console.log(User.getUserCountString()); // 2 instance(s) created.
静的メソッドは、インスタンスからではなく、クラス名から直接呼び出します(User.getUserCountString())。
staticは「クラス全体に紐づくプロパティやメソッド」を定義するためのキーワードです。
9. ゲッター・セッター
ゲッター・セッターは、プロパティの取得や設定時に処理を挟むことができる仕組みです。
通常のプロパティアクセスとの違いを整理しながら理解していきます。
9.1 ゲッター・セッターの基本
プロパティへのアクセス時に、何らかのロジックを組み込みたい場合、ゲッター・セッターを使います。
class User {
name;
#score; // プライベートフィールド
constructor(name, score) {
this.name = name;
this.#score = score;
}
get score() {
return this.#score;
}
set score(newValue) {
this.#score = newValue;
}
}
const user1 = new User("Taro", 70);
console.log(user1.score); // 70
user1.score = 85; // セッターが呼ばれる
console.log(user1.score); // 85
get score()は値を取得するとき、set score(newValue)は値を設定するときに呼ばれます。内部では#scoreという別名でプロパティを保持し、外部からはscoreという名前でアクセスできるようにしています。
9.2 バリデーションの追加
セッターにバリデーション処理を追加してみます。
class User {
name;
#score;
constructor(name, score) {
this.name = name;
this.#score = score;
}
get score() {
return this.#score;
}
set score(newValue) {
// 0〜100の範囲外の場合はエラー
if (newValue < 0 || newValue > 100) {
console.log("無効な値です");
return;
}
this.#score = newValue;
}
}
const user1 = new User("Taro", 70);
console.log(user1.score); // 70
user1.score = 150; // 無効な値です
console.log(user1.score); // 70(変更されない)
user1.score = 85;
console.log(user1.score); // 85
0〜100以外の値を設定しようとすると、エラーメッセージが表示され、値は更新されません。これにより、不正なデータの混入を防げます。
10. プライベートフィールド(カプセル化)
プライベートフィールドは、クラスの外からアクセスできないプロパティやメソッドを定義する機能です。
内部実装の詳細を隠蔽し、意図しない変更を防ぐことを「カプセル化」と呼びます。
10.1 プライベートフィールドの導入
#を使うことで、クラスの外からアクセスできないプロパティを作れます。
class User {
name;
#score; // プライベートフィールド
constructor(name, score) {
this.name = name;
this.#score = score;
}
get score() {
return this.#score;
}
set score(newValue) {
if (newValue < 0 || newValue > 100) {
console.log("無効な値です");
return;
}
this.#score = newValue;
}
}
const user1 = new User("Taro", 70);
console.log(user1.score); // 70
#scoreはクラスの外から直接アクセスできません。もしuser1.#score = 200;のように書くとエラーになります。このように、外部からのアクセスを制限することを「カプセル化」と呼びます。
10.2 プライベートメソッド
メソッドにも#を使えます。
class User {
name;
#score;
constructor(name, score) {
this.name = name;
this.#score = score;
}
get score() {
return this.#score;
}
set score(newValue) {
if (!this.#isValueValid(newValue)) {
console.log("無効な値です");
return;
}
this.#score = newValue;
}
// プライベートメソッド(クラス内部でのみ使用)
#isValueValid(value) {
return value >= 0 && value <= 100;
}
}
const user1 = new User("Taro", 70);
console.log(user1.score); // 70
user1.score = 150; // 無効な値です
console.log(user1.score); // 70
user1.score = 85;
console.log(user1.score); // 85
#isValueValid()はクラス内部でのみ使うメソッドなので、プライベートメソッドとして定義しました。これにより、内部実装の詳細を隠蔽しつつ、コードの見通しを良くできます。
プライベートフィールド・メソッドはクラスの外から呼び出すとエラーになります。
まとめ
JavaScriptのクラスについて、基本から応用まで一通り学習しました。
- クラスの基本: constructor、プロパティ、メソッドの定義
- クラスの組み合わせ: 複数のクラスを連携させる設計
- 継承: extendsとsuperを使った機能の拡張
- static: クラス全体で共有するプロパティ・メソッド
- ゲッター・セッター: プロパティアクセス時のロジック追加
- カプセル化: プライベートフィールド・メソッドによる隠蔽
最初は少し難しく感じましたが、実際にコードを書いて試すことで理解が深まりました。特に継承やカプセル化といった概念は、大きなアプリケーションを作る際に重要になってくるようです。
今後も実践的なコードを書きながら、クラスの使い方に慣れていきたいと思います。