JavaScriptの基礎 - クラス
概要
ES2015 (ES6) で導入された class 構文は、JavaScriptにおけるクラスベースのオブジェクト生成を簡潔に記述するためのシンタックスシュガーである。
JavaScriptの内部的なオブジェクト機構はプロトタイプベースであり、class構文はその仕組みをより直感的な形式で表現するためのものである。
クラスは constructor メソッドによるインスタンス初期化、インスタンスメソッド、ゲッター / セッターといった基本機能を備えている。
ES2022では、クラスボディでのパブリックフィールド宣言およびプライベートメンバー (# 記法) が正式仕様として標準化された。
また、static キーワードにより、インスタンスではなくクラス自体に属する静的メソッドおよび静的プロパティを定義できる。
クラス宣言は関数宣言とは異なりホイスティング後もTDZ (Temporal Dead Zone) に入るため、定義より前にクラスを使用するとエラーになる点に注意が必要である。
クラスボディは自動的にstrictモードで実行される。
現代のReact開発では関数コンポーネントとHooksが主流であるが、ErrorBoundaryのような特定の機能にはクラスコンポーネントが今なお必須である。
クラスの継承 (extends / super) やプロトタイプチェーンの詳細については、JavaScriptの基礎 - 継承とプロトタイプのページを参照すること。
クラスの基本構文
クラスの基本的な宣言方法とコンストラクタ、メソッドの定義について以下に示す。
クラスの宣言
class キーワードを使用してクラスを宣言する。
クラスボディ ({ } の内部) には、コンストラクタやメソッドを定義する。
クラス宣言の主な特性を以下に示す。
| 項目 | 説明 |
|---|---|
| ホイスティングの挙動 | 関数宣言とは異なり、class 宣言はホイスティングされるが、TDZ (Temporal Dead Zone) に入る。 |
| 定義より前にクラスを使用すると ReferenceError が発生する。 | |
| strictモード | クラスボディは自動的にstrictモードで実行される。"use strict" を明示する必要はない。
|
基本的なクラス宣言の例を以下に示す。
// クラス宣言
class Person {
constructor(name, age) {
this.name = name; // インスタンスのプロパティを初期化する
this.age = age;
}
// インスタンスメソッド
greet() {
console.log("こんにちは、" + this.name + "です。年齢は" + this.age + "歳です。");
}
}
const taro = new Person("太郎", 25);
taro.greet(); // "こんにちは、太郎です。年齢は25歳です。"
ホイスティングの挙動の例を以下に示す。
// クラスを定義より前に使用すると ReferenceError が発生する
const p = new Person("太郎", 25); // ReferenceError: Cannot access 'Person' before initialization
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
コンストラクタ
constructor メソッドは、new 演算子でクラスのインスタンスを生成する際に自動的に呼び出される特別なメソッドである。
インスタンスのプロパティの初期化等、オブジェクト生成時の処理を記述する。
下表に、コンストラクタに関する主な仕様を示す。
| 項目 | 説明 |
|---|---|
| new演算子の必須 | new 演算子を使用せずにクラスを呼び出すと TypeError が発生する。
|
| デフォルトコンストラクタ | constructor を省略した場合、空のデフォルトコンストラクタが自動的に挿入される。 |
継承クラスでコンストラクタを省略した場合は、super(...args) を呼び出すコンストラクタが自動挿入される。
|
コンストラクタの使用例を以下に示す。
class Car {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.speed = 0; // 初期値を設定する
}
accelerate(amount) {
this.speed += amount;
console.log(this.make + " " + this.model + " の速度: " + this.speed + " km/h");
}
brake(amount) {
this.speed = Math.max(0, this.speed - amount);
console.log(this.make + " " + this.model + " の速度: " + this.speed + " km/h");
}
}
const car = new Car("Toyota", "Prius", 2023);
car.accelerate(60); // "Toyota Prius の速度: 60 km/h"
car.brake(20); // "Toyota Prius の速度: 40 km/h"
// new無しで呼び出すとTypeErrorが発生する
// const car2 = Car("Toyota", "Prius", 2023); // TypeError: Class constructor Car cannot be invoked without 'new'
メソッドの定義
クラスで定義したインスタンスメソッドは、クラスのプロトタイプオブジェクトに追加される。
これにより、全インスタンスで同一のメソッドが共有される。
ゲッター (get) と セッター (set) を使用することにより、プロパティへのアクセスに処理を挟むことができる。
ゲッターは計算プロパティやバリデーションに、セッターは値の検証や変換に活用される。
getter / setterを使用したクラスの例を以下に示す。
class Circle {
constructor(radius) {
// セッターを経由してバリデーションを適用する
this.radius = radius;
}
// セッター : radiusに代入する際に呼び出される
set radius(value) {
if (value < 0) {
throw new Error("半径は0以上の値を指定してください");
}
this._radius = value;
}
// ゲッター : radiusを参照する際に呼び出される
get radius() {
return this._radius;
}
// 計算プロパティとしてのゲッター (面積を都度計算する)
get area() {
return Math.PI * this._radius ** 2;
}
// 計算プロパティとしてのゲッター (円周を都度計算する)
get circumference() {
return 2 * Math.PI * this._radius;
}
}
const c = new Circle(5);
console.log(c.radius); // 5
console.log(c.area.toFixed(2)); // "78.54"
c.radius = 10;
console.log(c.area.toFixed(2)); // "314.16"
// c.radius = -1; // Error : 半径は0以上の値を指定してください
下表に、メソッドの種類と用途の比較を示す。
| 種類 | 構文 | 呼び出し方 | 用途 |
|---|---|---|---|
| インスタンスメソッド | methodName() { ... } |
instance.methodName() |
インスタンスに対する操作 |
| ゲッター | get propName() { ... } |
instance.propName |
計算プロパティ、値の読み取り |
| セッター | set propName(v) { ... } |
instance.propName = value |
値の検証、加工して保存 |
フィールド宣言
ES2022で正式仕様化されたクラスフィールド宣言を使用すると、constructor 外のクラスボディにフィールドを直接定義できる。
フィールドはプロトタイプではなく各インスタンスのプロパティとして設定される。
パブリックフィールド
パブリックフィールドは、クラスボディの先頭でフィールド名と初期値を宣言することで定義する。
constructor 内での this.propName = value の記述が不要になり、クラスの構造をより明確に表現できる。
下表に、パブリックフィールドの特性を示す。
| 特性 | 説明 |
|---|---|
| インスタンスプロパティとして設定される | フィールドはプロトタイプではなく、各インスタンスに直接付与される。 |
| デフォルト値の設定 | フィールド宣言時に初期値を設定できる。 初期値を省略すると、 undefined になる。
|
パブリックフィールド宣言の例を以下に示す。
class Temperature {
// パブリックフィールド (デフォルト値を設定する)
unit = "celsius";
value = 0;
constructor(value, unit = "celsius") {
this.value = value;
this.unit = unit;
}
toCelsius() {
if (this.unit === "fahrenheit") {
return (this.value - 32) * 5 / 9;
}
return this.value;
}
toFahrenheit() {
if (this.unit === "celsius") {
return this.value * 9 / 5 + 32;
}
return this.value;
}
toString() {
return this.value + "°" + this.unit.charAt(0).toUpperCase();
}
}
const temp = new Temperature(100, "celsius");
console.log(temp.toString()); // "100°C"
console.log(temp.toFahrenheit()); // 212
プライベートフィールドとメソッド (#)
# 記法を使用することで、クラス外部からアクセスできないプライベートメンバーを定義できる。
ES2022で正式仕様化され、カプセル化を強制する手段として広く使用される。
プライベートメンバーの特性を以下に示す。
| 特性 | 説明 |
|---|---|
| クラス外からのアクセス禁止 | クラス外部から # フィールド や # メソッドにアクセスしようとするとSyntaxError が発生する。 |
| プライベートメソッドも同様 | メソッドにも # を付与することで、クラス内部からのみ呼び出せるプライベートメソッドを定義できる。 |
| カプセル化の意義 | 内部実装の詳細を隠蔽することにより、 クラスの外部インターフェースを明確に保ち、保守性を高める。 |
プライベートフィールドとメソッドの使用例を以下に示す。
class BankAccount {
// プライベートフィールド
#balance;
#owner;
#transactionHistory = [];
constructor(owner, initialBalance = 0) {
this.#owner = owner;
this.#balance = initialBalance;
}
// プライベートメソッド (内部処理のみで使用する)
#recordTransaction(type, amount) {
this.#transactionHistory.push({
type,
amount,
balance : this.#balance,
timestamp : new Date().toISOString()
});
}
// パブリックメソッド (外部から呼び出すインターフェース)
deposit(amount) {
if (amount <= 0) {
throw new Error("入金額は0より大きい値を指定してください");
}
this.#balance += amount;
this.#recordTransaction("deposit", amount);
console.log(this.#owner + " さんの残高: " + this.#balance + " 円");
}
withdraw(amount) {
if (amount > this.#balance) {
throw new Error("残高が不足しています");
}
this.#balance -= amount;
this.#recordTransaction("withdraw", amount);
console.log(this.#owner + " さんの残高: " + this.#balance + " 円");
}
get balance() {
return this.#balance;
}
}
const account = new BankAccount("太郎", 10000);
account.deposit(5000); // "太郎 さんの残高: 15000 円"
account.withdraw(3000); // "太郎 さんの残高: 12000 円"
// console.log(account.#balance); // SyntaxError: クラス外からはアクセスできない
静的メソッドとプロパティ
static キーワードを使用することで、インスタンスではなくクラス自体に属するメソッドとプロパティを定義できる。
静的メンバーは、クラス名から直接アクセスする。
静的メソッド
静的メソッドは、インスタンスを生成せずにクラスから直接呼び出せるメソッドである。
ファクトリパターン (特定の条件でインスタンスを生成するメソッド) や ユーティリティ関数としての用途に適している。
静的メソッドの使用例を以下に示す。
class User {
constructor(name, email, role) {
this.name = name;
this.email = email;
this.role = role;
}
// ファクトリメソッド : 管理者ユーザを生成する
static createAdmin(name, email) {
return new User(name, email, "admin");
}
// ファクトリメソッド : 一般ユーザを生成する
static createGuest(name) {
return new User(name, "guest@example.com", "guest");
}
// ユーティリティメソッド: メールアドレスの検証
static isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
toString() {
return this.name + " (" + this.role + ")";
}
}
// インスタンスを生成せずに静的メソッドを呼び出す
const admin = User.createAdmin("管理者", "admin@example.com");
const guest = User.createGuest("ゲスト");
console.log(admin.toString()); // "管理者 (admin)"
console.log(guest.toString()); // "ゲスト (guest)"
console.log(User.isValidEmail("test@example.com")); // true
静的プロパティ
静的プロパティは、クラスレベルで共有される値を保持するために使用する。
クラス全体に共通する定数やカウンタの管理に適している。
静的プロパティの使用例を以下に示す。
class DatabaseConnection {
// 静的プロパティ: クラス全体で共有する設定値
static MAX_CONNECTIONS = 10;
static #instanceCount = 0; // プライベートな静的プロパティ
constructor(host, port) {
if (DatabaseConnection.#instanceCount >= DatabaseConnection.MAX_CONNECTIONS) {
throw new Error("接続数の上限 (" + DatabaseConnection.MAX_CONNECTIONS + ") に達しました");
}
this.host = host;
this.port = port;
DatabaseConnection.#instanceCount++;
console.log("接続を確立しました (現在の接続数: " + DatabaseConnection.#instanceCount + ")");
}
// 静的メソッドから静的プロパティにアクセスする
static getInstanceCount() {
return DatabaseConnection.#instanceCount;
}
close() {
DatabaseConnection.#instanceCount--;
console.log("接続を閉じました (現在の接続数: " + DatabaseConnection.#instanceCount + ")");
}
}
const db1 = new DatabaseConnection("localhost", 5432); // "接続を確立しました (現在の接続数: 1)"
const db2 = new DatabaseConnection("localhost", 5433); // "接続を確立しました (現在の接続数: 2)"
console.log(DatabaseConnection.getInstanceCount()); // 2
console.log(DatabaseConnection.MAX_CONNECTIONS); // 10
db1.close(); // "接続を閉じました (現在の接続数: 1)"
下表に、インスタンスメンバーと静的メンバーの比較を示す。
| 項目 | インスタンスメンバー | 静的メンバー |
|---|---|---|
| 定義方法 | 通常の定義 | static キーワードを付与
|
| アクセス方法 | instance.member |
ClassName.member
|
| 共有範囲 | 各インスタンスが個別に保持 | クラス全体で1つを共有 |
| 用途 | インスタンス固有のデータ・処理 | ユーティリティ、定数、ファクトリ |
this の参照先 |
インスタンスを参照する | クラス自体を参照する |
クラス式
クラスは宣言 (class文) だけでなく、式 (クラス式) としても記述できる。
クラス式は変数に代入したり、関数の引数として渡したりすることができる。
クラス式には無名クラス式と名前付きクラス式の2種類がある。
名前付きクラス式の名前はクラスの内部 (メソッド内の再帰呼び出し等) からのみアクセスでき、クラス外部からは参照できない。
クラス式の例を以下に示す。
// 無名クラス式 : 変数に代入する
const Animal = class {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
speak() {
console.log(this.name + " は " + this.sound + " と鳴く");
}
};
const dog = new Animal("犬", "ワン");
dog.speak(); // "犬 は ワン と鳴く"
// 名前付きクラス式 : クラス名はクラス内部からのみ参照できる
const Fibonacci = class FibClass {
constructor(n) {
this.n = n;
}
compute() {
if (this.n <= 1) return this.n;
// 内部からは、FibClassという名前でアクセスできる
return new FibClass(this.n - 1).compute() + new FibClass(this.n - 2).compute();
}
};
const fib = new Fibonacci(10);
console.log(fib.compute()); // 55
// クラス外部からFibClassという名前にはアクセスできない
// console.log(FibClass); // ReferenceError
Reactとクラス構文
React.Componentを継承することで、クラスコンポーネントを作成できる。
クラスコンポーネントでは、constructor で this.state を初期化し、render メソッドでJSXを返す。
現在は関数コンポーネントとHooksが主流であり、クラスコンポーネントを新規に作成する場面は少ない。
ただし、ErrorBoundaryのようにライフサイクルメソッド (componentDidCatch / getDerivedStateFromError) を必要とする機能は、クラスコンポーネントでのみ実装できる。
ErrorBoundaryの詳細については、JavaScriptの基礎 - 継承とプロトタイプのページを参照すること。
シンプルなReactクラスコンポーネントの例を以下に示す。
import React from "react";
class Counter extends React.Component {
// パブリックフィールドでstateを宣言する (constructor不要)
state = {
count : 0
};
// クラスフィールドとアロー関数でthisを固定する
handleIncrement = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
};
handleDecrement = () => {
this.setState(prevState => ({ count: prevState.count - 1 }));
};
handleReset = () => {
this.setState({ count: 0 });
};
// renderメソッドでJSXを返す
render() {
return (
<div>
<p>カウント: {this.state.count}</p>
<button onClick={this.handleIncrement}>+1</button>
<button onClick={this.handleDecrement}>-1</button>
<button onClick={this.handleReset}>リセット</button>
</div>
);
}
}
export default Counter;
関連情報
- JavaScriptの基礎 - 継承とプロトタイプ
- クラスの継承 (extends/super)、プロトタイプチェーン、ReactのErrorBoundary
- JavaScriptの基礎 - thisキーワード
- thisの動作、bind/call/apply、クラスにおけるthis
- JavaScriptの基礎 - オブジェクトリテラル
- オブジェクトの作成と操作、プロパティアクセス、メソッド
- JavaScriptの基礎 - アロー関数
- アロー関数の構文、レキシカルthis