JavaScript中級 #1 クラス

読了 7分

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 と一緒に使います。

new 必須
User('カーティス', 30);   // ✗ TypeError
new User('カーティス', 30);  // OK

メソッドと this #

メソッドの中の 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 またはクラスフィールドでアロー関数 #

フィールドのアロー — thisが束縛されたまま固定される
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 — プロパティのように見えるメソッド #

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 #

インスタンスではなくクラス自体に付くメソッド/プロパティ。

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 { ... } ブロックを使えます。

static ブロック
class Config {
  static defaults = {};

  static {
    Config.defaults = JSON.parse(loadConfigFile());
    Config.defaults.timestamp = Date.now();
  }
}

よく使うわけではありませんが、ライブラリの初期化の場面で時々見かけます。

Privateフィールド — # #

ES2022 で追加された正式なprivate構文。外部からアクセスすること自体が不可能になります。

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;            // 実はアクセス可能

新しいコードでは # を使うのが安全です。

継承 — extendssuper #

継承
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 — クラスチェック #

instanceof
const d = new Dog('ポチ', '柴犬');

d instanceof Dog;       // true
d instanceof Animal;    // true (親も true)
d instanceof Object;    // true
d instanceof User;       // false

継承チェーンを辿って検査します。クラスで作られたインスタンスかどうかを確認するときによく使います。

クラス vs オブジェクトリテラル #

同じことをしようとするとき、クラスとただのオブジェクトのどちらが合うかのガイド。

場面合う側
同じ形のインスタンスを複数作るクラス
動作(メソッド)がデータに強く結びついているクラス
単一インスタンス、設定オブジェクトのような場面オブジェクトリテラル
関数型で変換だけを繰り返す場面ただの関数 + オブジェクト
継承が必要なほど階層が深いクラス (またはコンポジション再考)

最初はクラスを使わなくても十分な場合が多いです。本当にクラスが合う場面に出会ったときに取り出して使うと考えればよいです。

まとめ #

この記事で整理した内容:

  • classnewconstructor の中で this 初期化
  • メソッドを切り離すと this が消える — アロー関数またはフィールドアロー
  • get/set でプロパティのように見えるメソッド
  • static でクラス自体に付くメンバー、static {} ブロック (ES2022)
  • #フィールド で本物のprivate (_アンダースコア は約束に過ぎない)
  • extends で継承、super(...) を先に呼ぶ
  • 継承の乱用注意 — モダン慣習はコンポジション優先
  • クラスが合う場面 vs オブジェクトリテラル

次の記事(#2 非同期入門)では、JavaScriptの最大の特徴の一つである非同期 — Promise と async/await を最初から整理します。

X