JavaScript中級 #1 クラス
JavaScript中級シリーズの最初の記事です。基礎7編を終えていれば、すでに小さなツールやスクリプトは自信を持って書けるはずですが、中級はその上にモダンJavaScriptの表現力を載せる場です。
全7編で構成されます。
- #1 クラス ← この記事
- #2 非同期入門 — Promise、async/await
- #3 イテレータとジェネレータ
- #4 デストラクチャリング/spread/rest 詳細
- #5 オプショナルチェーンとnullish合体
- #6 fetch API とエラー処理
- #7 JSONの扱いとシリアライズ
この記事では — JavaScriptのクラス構文を最初から最後まで整理します。
クラス基礎 #
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`こんにちは、${this.name} (${this.age})`);
}
}
const u = new User('カーティス', 30);
u.greet(); // こんにちは、カーティス (30)
class キーワードで始まり、constructor がインスタンスを作るときに呼び出される初期化関数です。本文の中の this は新しく作られるインスタンスを指します。
new なしで呼び出すとエラーになります — クラスは常に new と一緒に使います。
User('カーティス', 30); // ✗ TypeError
new User('カーティス', 30); // OK
メソッドと this
#
メソッドの中の this は呼び出すインスタンスを指すのが基本です。ただし関数を切り離して呼び出すと this が消えることがあります。
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`こんにちは、${this.name}`);
}
}
const u = new User('カーティス');
u.greet(); // こんにちは、カーティス
const fn = u.greet; // 切り離す
fn(); // こんにちは、undefined ← this が消える
これがJavaScriptクラスで最もよくある落とし穴です。コールバックとしてメソッドを渡したり、イベントハンドラとして登録するときに頻繁に出会います。解決方法は次の二つ。
1) アロー関数で包む #
button.addEventListener('click', () => u.greet());2) bind またはクラスフィールドでアロー関数
#
class User {
constructor(name) {
this.name = name;
}
greet = () => {
console.log(`こんにちは、${this.name}`);
};
}
const u = new User('カーティス');
const fn = u.greet;
fn(); // こんにちは、カーティス — 切り離しても this が生きている
このパターンはReactのコールバックの場面でよく見かけるはずです。ただしメソッドがインスタンスごとに新しく作られるコストがあるため、重いクラスには推奨されません。
getter / setter — プロパティのように見えるメソッド #
class Temperature {
constructor(celsius) {
this._celsius = celsius;
}
get fahrenheit() {
return this._celsius * 9 / 5 + 32;
}
set fahrenheit(value) {
this._celsius = (value - 32) * 5 / 9;
}
}
const t = new Temperature(25);
console.log(t.fahrenheit); // 77 — メソッド呼び出しではなくプロパティのようにアクセス
t.fahrenheit = 100;
console.log(t._celsius); // 約 37.7
get / set キーワードで定義すると、呼び出し時に括弧なしでプロパティのように読み書きできます。計算して返す、または設定時に検証するときに合います。
静的メンバー — static
#
インスタンスではなくクラス自体に付くメソッド/プロパティ。
class MathUtil {
static PI = 3.14159;
static square(n) {
return n * n;
}
}
MathUtil.PI; // 3.14159
MathUtil.square(5); // 25
const m = new MathUtil();
m.square(5); // ✗ インスタンスにはない
ユーティリティ関数のまとめ、またはインスタンス生成を助けるファクトリーメソッド(例: User.fromJSON(...))の場面に合います。
static ブロック — ES2022
#
静的メンバーを作るときに複雑な初期化ロジックが必要なら、static { ... } ブロックを使えます。
class Config {
static defaults = {};
static {
Config.defaults = JSON.parse(loadConfigFile());
Config.defaults.timestamp = Date.now();
}
}よく使うわけではありませんが、ライブラリの初期化の場面で時々見かけます。
Privateフィールド — #
#
ES2022 で追加された正式なprivate構文。外部からアクセスすること自体が不可能になります。
class BankAccount {
#balance = 0;
deposit(amount) {
this.#balance = this.#balance + amount;
}
get balance() {
return this.#balance;
}
}
const acc = new BankAccount();
acc.deposit(100);
console.log(acc.balance); // 100
console.log(acc.#balance); // ✗ SyntaxError — 外部アクセス不可
名前の前に # を付けます。慣例ではなく言語レベルで強制される本当のprivateです。古いJavaScriptの「慣習的private (_balance)」と異なり、外部からアクセスすること自体が遮断されます。
_アンダースコア との違い
#
古いコードでよく見られる _balance のようなものは約束に過ぎません。
class OldStyle {
constructor() {
this._balance = 0; // 慣習: アクセスしないでください
}
}
const o = new OldStyle();
o._balance = 999; // 実はアクセス可能
新しいコードでは # を使うのが安全です。
継承 — extends と super
#
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} が音を出します`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 親 constructor 呼び出し
this.breed = breed;
}
speak() {
super.speak(); // 親メソッド呼び出し
console.log(`${this.name} が吠えます (ワンワン)`);
}
}
const d = new Dog('ポチ', '柴犬');
d.speak();
// ポチ が音を出します
// ポチ が吠えます (ワンワン)
extends で親クラスを指定し、子のconstructorの中では super(...) を先に呼んでから this を使えるようになります。これを忘れるとReferenceErrorが発生します。
super.method() で親のメソッドを呼び出すことができ、子が同じ名前のメソッドを定義すると親のものを覆います(オーバーライド)。
継承 — 深く使う価値があるか? #
継承はJavaScriptだけでなくOOP全般で乱用するとコードがすぐ壊れるツールです。モダンJavaScriptは次のことを推奨します。
- 単一クラスで十分な場面でわざわざ継承を作らない
- 継承の代わりにコンポジション(composition) — オブジェクトに別のオブジェクトを持たせる
- インターフェース/共通シグネチャが必要なら関数とオブジェクトでも十分
Reactがクラスコンポーネント → 関数コンポーネントへ移行したのも似た文脈です。クラスが必須の場合は意外と多くありません。
instanceof — クラスチェック
#
const d = new Dog('ポチ', '柴犬');
d instanceof Dog; // true
d instanceof Animal; // true (親も true)
d instanceof Object; // true
d instanceof User; // false
継承チェーンを辿って検査します。クラスで作られたインスタンスかどうかを確認するときによく使います。
クラス vs オブジェクトリテラル #
同じことをしようとするとき、クラスとただのオブジェクトのどちらが合うかのガイド。
| 場面 | 合う側 |
|---|---|
| 同じ形のインスタンスを複数作る | クラス |
| 動作(メソッド)がデータに強く結びついている | クラス |
| 単一インスタンス、設定オブジェクトのような場面 | オブジェクトリテラル |
| 関数型で変換だけを繰り返す場面 | ただの関数 + オブジェクト |
| 継承が必要なほど階層が深い | クラス (またはコンポジション再考) |
最初はクラスを使わなくても十分な場合が多いです。本当にクラスが合う場面に出会ったときに取り出して使うと考えればよいです。
まとめ #
この記事で整理した内容:
classとnew、constructorの中でthis初期化- メソッドを切り離すと
thisが消える — アロー関数またはフィールドアロー get/setでプロパティのように見えるメソッドstaticでクラス自体に付くメンバー、static {}ブロック (ES2022)#フィールドで本物のprivate (_アンダースコアは約束に過ぎない)extendsで継承、super(...)を先に呼ぶ- 継承の乱用注意 — モダン慣習はコンポジション優先
- クラスが合う場面 vs オブジェクトリテラル
次の記事(#2 非同期入門)では、JavaScriptの最大の特徴の一つである非同期 — Promise と async/await を最初から整理します。