JavaScript 入門

JS ES6 クラス入門

ES6で class キーワードが導入される前、JavaScriptは主にプロトタイプチェーン(Prototype)を通じてオブジェクト指向プログラミングを実現していました。

ES6のクラス構文は、本質的にはJavaScriptのプロトタイプ継承の 構文糖衣(シンタックスシュガー) です。これにより、オブジェクトのテンプレートを定義するための、より明確で直感的な構造が提供されます。クラスを利用することで、開発者はデータのカプセル化、コードの再利用、および複雑な状態ロジックの管理をより効率的に行うことができます。

1. ES6 クラスの基本構文

ES6の class キーワードは、JavaScriptにおいてより標準的で読みやすいオブジェクトテンプレートの定義方法を提供します。

その内部メカニズムは依然としてプロトタイプチェーンに基づいた継承ですが、この構文によって、コード構造は従来のオブジェクト指向言語(JavaやC#など)に非常に近いものとなりました。

1.1 クラスの宣言とインスタンス化

クラスを定義するには class キーワードを使用し、その後にクラス名を記述します。new キーワードを使用することで、クラスに基づいたインスタンスを作成できます。

class User {
  // コンストラクタ:プロパティの初期化に使用
  constructor(name) {
    this.name = name;
  }

  // インスタンスメソッド:クラスのプロトタイプ上に定義される
  sayHello() {
    console.log(`こんにちは、私は ${this.name} です。`);
  }
}

// インスタンス化
const user = new User('アリス');
user.sayHello(); // 出力: こんにちは、私は アリス です。

2. コンストラクタ (Constructor)

ES6クラスにおいて、constructor(コンストラクタ)はオブジェクトを作成し初期化するための「エンジン」です。new キーワードを使用してクラスを呼び出すたびに、このメソッドが自動的に実行されます。

2.1 基本的なコンストラクタ:プロパティの初期化

コンストラクタの最も一般的な用途は、渡された引数をインスタンス(this)に割り当てることです。

class Car {
  constructor(brand, color) {
    this.brand = brand; // 引数をインスタンスプロパティに代入
    this.color = color;
  }
}

const myCar = new Car('テスラ', 'レッド');
console.log(myCar.brand); // 出力: テスラ

2.2 コンストラクタの特徴

2.2.1 デフォルトコンストラクタ

クラス定義時に constructor を記述しなかった場合、JavaScriptはバックグラウンドで空のコンストラクタを自動的に生成します。

class EmptyClass {
  // エンジンが自動的に追加する:
  // constructor() {}
}

2.2.2 唯一性

一つのクラス内には、一つの constructor メソッドしか存在できません。二つ定義すると SyntaxError がスローされます。

2.2.3 戻り値の特殊な挙動

コンストラクタはデフォルトで this(新しく作成されたインスタンス)を返します。しかし、手動で別のオブジェクトを return した場合、new の結果はその返されたオブジェクトになり、元のインスタンスではなくなります。

class Box {
  constructor() {
    this.type = 'ボックス';
    // 手動で全く別のオブジェクトを返す
    return { name: 'カスタムオブジェクトです' };
  }
}

const item = new Box();
console.log(item.name); // 出力: カスタムオブジェクトです
console.log(item.type); // 出力: undefined

2.3 継承における使用 (super)

extends を使用して継承を行う場合、子クラスのコンストラクタ内で必ず super() を呼び出す必要があります。これは親クラスのコンストラクタを呼び出し、親クラスのプロパティを初期化する役割を担います。呼び出さないとエラーが発生します。

class Animal {
  constructor(name) {
    this.name = name;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 最初に親クラスのコンストラクタを呼び出す必要がある
    this.breed = breed;
  }
}

3. インスタンスメソッド

インスタンスメソッドはクラスのプロトタイプ(Prototype)上に定義され、new によって作成されたすべてのインスタンスから呼び出すことができます。これらのメソッドは this を通じて現在のインスタンスのプロパティにアクセスできます。

クラス内で定義された関数は、自動的にインスタンスメソッドとなります。

  • 共有性: これらのメソッドはクラスのプロトタイプオブジェクトに配置されるため、すべてのインスタンスで一つのメソッド定義を共有し、メモリを節約します。
  • this の指向: インスタンスメソッド内部の this は、現在呼び出されているインスタンスオブジェクトを指します。
class Counter {
  constructor() {
    this.count = 0;
  }

  increment() {
    this.count++;
  }
}

4. 静的メソッド

static キーワードを使用して定義されたメソッドはインスタンスに属さず、クラス自体に属します。

class Calculator {
  static sum(a, b) {
    return a + b;
  }
}

// インスタンス化せずにクラス名から直接呼び出す
console.log(Calculator.sum(1, 2)); // 出力: 3

用途: 通常、ユーティリティ関数や、インスタンスのプロパティ(this.xxx)に依存しないロジックを記述するために使用されます。

5. 継承 (Extends)

JavaScriptにおいて、継承(Inheritance) はオブジェクト指向プログラミングの核心です。あるクラス(子クラス)が別のクラス(親クラス)のプロパティやメソッドを受け継ぐことを可能にします。これによりコードの重複を減らし、階層構造によってプログラムの拡張性を高めることができます。

5.1 継承の基本構文

継承関係を構築するには extends キーワードを使用し、親クラスの機能を呼び出すには super を使用します。

  • extends: 「子クラスが親クラスを継承する」ことを示します。
  • super(): 子クラスの constructor 内で this を使用する前に必ず呼び出す必要があります。親クラスのコンストラクタを実行し、初期化を行います。

5.2 コード例:「従業員」から「マネージャー」へ

一般的な Employee クラスと、より具体的な Manager クラスを作成してみましょう。

// 親クラス
class Employee {
  constructor(name, salary) {
    this.name = name;
    this.salary = salary;
  }

  getDetails() {
    return `${this.name} の給与は ${this.salary} です`;
  }
}

// 子クラス:Employeeを継承
class Manager extends Employee {
  constructor(name, salary, department) {
    // 1. 親クラスのコンストラクタを呼び出し、nameとsalaryを初期化
    super(name, salary);
    
    // 2. 子クラス固有のプロパティを初期化
    this.department = department;
  }

  // 子クラスで親クラスのメソッドを上書き(オーバーライド)
  getDetails() {
    // 親クラスの元のメソッドを呼び出し、新しいロジックを追加
    return `${super.getDetails()}、所属部署:${this.department}`;
  }

  // 子クラス特有の新しいメソッド
  conductMeeting() {
    console.log(`${this.name} は ${this.department} の会議を進行しています。`);
  }
}

const mgr = new Manager('田中', 20000, '技術部');
console.log(mgr.getDetails()); // 出力: 田中 の給与は 20000 です、所属部署:技術部
mgr.conductMeeting();

5.3 継承を使用する理由

  1. コードの再利用 (DRY原則): Manager 内で namesalary の初期化ロジックを再記述する必要がなく、Employee のロジックをそのまま利用できます。
  2. メソッドのオーバーライド (Method Overriding): 子クラスのニーズに応じて、親クラスと同名のメソッドを書き換えることができます。
  3. 多態性 (Polymorphism): Employee 型を扱うコードを書けば、その子クラス(Manager, Developer など)すべてに適用できます。これらはすべて親クラスの基本特性を備えているからです。

5.4 重要な注意点

  • super の二重の役割:
    • 関数として呼び出す:super()。子クラスの constructor 内でのみ使用し、親クラスを初期化します。
    • オブジェクトとして使用:super.methodName()。子クラスのメソッド内で親クラスのプロトタイプにある同名メソッドを呼び出す際に使用します。
  • super() の呼び出し必須: 子クラスで constructor を定義した場合、super() を省略することはできません。
  • 単一継承の制限: JavaScriptのクラスは単一継承のみをサポートしています(一つのクラスが継承できる親クラスは一つだけです)。

6. アクセサ (Getters & Setters)

JavaScriptクラスにおける アクセサ(Getters & Setters) は、オブジェクトのプロパティへのアクセスや代入をインターセプト(捕捉)するための特殊な構文です。

これらはプロパティの「門番」のようなものです。これらを通じることで、データの取得や変更時に追加のロジック(バリデーション、ログ記録、フォーマット変換など)を挿入でき、外部からは通常のプロパティと同じように扱うことができます。

6.1 なぜ Getter と Setter が必要なのか?

プロパティに直接アクセスする場合(例:user.name = 'alice')、データの品質を制御できません。アクセサを使用することで以下が可能になります:

  • データのカプセル化: 内部に保存されている変数を隠蔽します。
  • 入力バリデーション: 代入される内容がビジネスルールに適合しているか確認します。
  • 算出プロパティ: プロパティ読み取り時に動的に値を計算します。

6.2 コード例

User クラスにおいて、年齢(age)が必ず 0 より大きくなるように制御します。

class User {
  constructor(name, age) {
    this.name = name;
    this._age = age; // アンダースコアは、外部から直接変更すべきでない「プライベート」な慣習を示します
  }

  // Getter:ageを読み取るときに発火
  get age() {
    return this._age;
  }

  // Setter:ageに代入するときに発火
  set age(value) {
    if (value < 0) {
      console.error("年齢を負の数にすることはできません!");
      return;
    }
    this._age = value;
  }
}

const user = new User('ボブ', 25);

// 通常のプロパティのように操作
console.log(user.age); // 25 (get age() が呼ばれる)

user.age = 30;         // set age(30) が呼ばれる
user.age = -5;         // set age(-5) が呼ばれ、エラーメッセージが出力され、代入は失敗する

6.3 通常のプロパティ vs. アクセサ

特徴通常のプロパティ (this.name = 'x')アクセサ (get/set)
実行ロジックメモリを直接読み書きカスタム関数を実行
データバリデーション不可代入時に論理チェックが可能
計算プロパティ不可読み取り時に動的計算が可能
呼び出し方obj.propobj.prop (呼び出し方は同じ)