クラス

クラスはオブジェクトを作成するためのテンプレートです。それらは、そのデータを処理するためのコードでデータをカプセル化します。JS のクラスはプロトタイプに基づいて構築されていますが、ES5 のクラスライクなセマンティクスとは共有されない構文やセマンティクスも持っています。

クラスの定義

クラスは実際には「特別な関数」であり、関数式関数宣言を定義することができるように、クラス構文にもクラス式クラス宣言の 2 つの定義方法があります。

クラス宣言

クラスを定義するひとつの方法は、クラス宣言を使うことです。クラスを宣言するには、クラス名 (この例では "Rectangle") 付きで class キーワードを使います。

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

ホイスティング(巻き上げ)

関数宣言クラス宣言の重要な違いは、関数宣言では Hoisting されるのに対し、クラス宣言ではされないことです。クラスにアクセスする前に、そのクラスを宣言する必要があります。そうしないと、ReferenceError が投げられます:

const p = new Rectangle(); // ReferenceError

class Rectangle {}

クラス式

クラスを定義する別の方法はクラス式です。クラス式は、名前付きでも名前なしでもできます。名前付きクラスの名前は、クラス内のローカルとして扱われます。(ただし (インスタンスのではなく) クラスの name プロパティによって取得可能)

// 名前なし
let Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// 出力: "Rectangle"

// 名前つき
let Rectangle = class Rectangle2 {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// 出力: "Rectangle2"

注: クラスにもクラス宣言で言及したのと同じホイスティング問題があります。

クラス本体とメソッド定義

中括弧 {} 内にクラス本体を記述します。クラス本体には、メソッドやコンストラクターといったクラスメンバを記述します。

Strict モード

クラス本体は Strict モード で実行されます。つまり、ここで書かれたコードは、パフォーマンスを向上させるために、より厳密な構文に従います。そうでない場合はサイレントエラーが投げられます。なお、特定のキーワードは将来のバージョンの ECMAScript 用に予約されています。

コンストラクター

コンストラクターメソッドは、class で作成したオブジェクトを作成して初期化するための特別なメソッドです。"constructor" という名前の特別なメソッドは、クラスに 1 つしか定義できません。クラスに複数のコンストラクターメソッドが存在する場合、SyntaxError が投げられます。

スーパークラスのコンストラクターは super というキーワードで呼び出せます。

プロトタイプメソッド

メソッド定義を参照してください。

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // ゲッター
  get area() {
    return this.calcArea();
  }
  // メソッド
  calcArea() {
    return this.height * this.width;
  }
}

const square = new Rectangle(10, 10);

console.log(square.area); // 100

静的メソッドとプロパティ

static キーワードは、クラスの静的メソッドまたはプロパティを定義します。静的メンバー(プロパティとメソッド)は、クラスをインスタンス化せずに呼び出され、クラスインスタンスを介して呼び出すことはできません。静的メソッドは、アプリケーションのユーティリティ関数を作成するためによく使用されますが、静的プロパティは、キャッシュ、固定構成、またはインスタンス間で複製する必要のないその他のデータに役立ちます。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static displayName = "Point";
  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;

    return Math.hypot(dx, dy);
  }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
p1.displayName; // undefined
p1.distance;    // undefined
p2.displayName; // undefined
p2.distance;    // undefined

console.log(Point.displayName);      // "Point"
console.log(Point.distance(p1, p2)); // 7.0710678118654755

プロトタイプと静的メソッドによるボクシング

this に値が付けられずに静的メソッドまたはプロトタイプメソッドが呼ばれると、this の値はメソッド内で undefined になります。たとえ "use strict" ディレクティブがなくても同じふるまいになります。なぜなら、class 本体の中のコードは常に Strict モードで実行されるからです。

class Animal {
  speak() {
    return this;
  }
  static eat() {
    return this;
  }
}

let obj = new Animal();
obj.speak(); // Animal {}
let speak = obj.speak;
speak(); // undefined

Animal.eat() // class Animal
let eat = Animal.eat;
eat(); // undefined

上のコードを従来の関数ベースの構文を使って書くと、非 Strict モードでは、最初の this の値をもとにして、メソッド呼び出しの中で自動ボクシングが行われます。最初の値が undefined の場合、this にはグローバルオブジェクトが入ります。

Strict モードでは自動ボクシングは行われません。this の値はそのまま渡されます。

function Animal() { }

Animal.prototype.speak = function() {
  return this;
}

Animal.eat = function() {
  return this;
}

let obj = new Animal();
let speak = obj.speak;
speak(); // グローバルオブジェクト(非厳格モード)

let eat = Animal.eat;
eat(); // グローバルオブジェクト(非厳格モード)

インスタンスプロパティ

インスタンスプロパティはクラスのメソッドの中で定義しなければなりません:

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

クラスに付随する静的なプロパティやプロトタイプのプロパティは、クラス本体の宣言の外で定義しなければなりません:

Rectangle.staticWidth = 20;
Rectangle.prototype.prototypeWidth = 25;

フィールド宣言

パブリックフィールドとプライベートフィールドの宣言は JavaScript 標準委員会の TC39 で提案されている実験的機能(ステージ 3)です。ブラウザーでのサポートは限られていますが、この機能は Babel のようなシステムでのビルドステップを通して使用できます。

パブリックフィールド宣言

JavaScript のフィールド宣言構文を使って、上記の例は次のように書くことができます。

class Rectangle {
  height = 0;
  width;
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

フィールドを事前宣言することで、クラス定義はより自己文書化され、フィールドは常に存在するようになります。

上記のように、フィールドはデフォルト値の有無にかかわらず宣言できます。

詳しい情報は、パブリッククラスフィールドを参照してください。

プライベートフィールド宣言

プライベートフィールドを使うと、宣言は下記のように洗練できます。

class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {
    this.#height = height;
    this.#width = width;
  }
}

プライベートフィールドの参照はクラス本体内でのみ可能となり、クラス外からの参照はエラーとなります。クラス外からは見えないものを定義することで、クラスのユーザーが(変更される可能性のある)内部状態に依存できないようにします。

プライベートフィールドは、事前宣言のみ可能です。

プライベートフィールドは通常のプロパティとは違い、this への追加によって後から作成することができません。

詳しい情報は、プライベートクラスフィールドを参照してください。

extends によるサブクラス

extends キーワードは、クラスを別クラスの子として作成するために、クラス宣言またはクラス式の中で使います。

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

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // スーパークラスのコンストラクターを呼び出し、name パラメータを渡す
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.

サブクラスにコンストラクターが存在する場合は、"this" を使う前に super() を呼ぶ必要があります。

従来の関数ベースの「クラス」も拡張できます:

function Animal (name) {
  this.name = name;
}

Animal.prototype.speak = function () {
  console.log(`${this.name} makes a noise.`);
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.

// 同様なメソッドでは、子のメソッドが親のメソッドよりも優先されます。

クラスは通常の (生成不可能な) オブジェクトを拡張できないことに注意してください。通常のオブジェクトから継承したければ、代わりに Object.setPrototypeOf() を使います:

const Animal = {
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
};

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

// このコードが無いと、speak() を実行した時に TypeError になります。
Object.setPrototypeOf(Dog.prototype, Animal);

let d = new Dog('Mitzie');
d.speak(); // Mitzie makes a noise.

Species

Array の派生クラスである MyArray の中で Array オブジェクトを返したいときもあるでしょう。species パターンは、デフォルトコンストラクタ-を上書きすることができます。

例えば、デフォルトコンストラクターを返す map() のようなメソッドを使っているとき、MyArray ではなく Array オブジェクトを返したいでしょう。Symbol.species シンボルを使うと次のように実現できます。

class MyArray extends Array {
  // species を親の Array コンストラクターで上書きする
  static get [Symbol.species]() { return Array; }
}

let a = new MyArray(1,2,3);
let mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array);   // true

super でスーパークラスを呼び出す

super キーワードを使ってスーパークラスのメソッドを呼び出せます。これはプロトタイプベースの継承よりも優れています。

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

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(`${this.name} roars.`);
  }
}

let l = new Lion('Fuzzy');
l.speak();
// Fuzzy makes a noise.
// Fuzzy roars.

ミックスイン

抽象的なサブクラスやミックスインはクラスのためのテンプレートです。ECMAScript のクラスは 1 つだけスーパークラスを持つことができます。そのため、多重継承はできません。機能はスーパークラスから提供されます。

ECMAScript では、スーパークラスをインプットとして、そしてスーパークラスを継承した派生クラスをアウトプットとする関数を mix-in で実装できます:

let calculatorMixin = Base => class extends Base {
  calc() { }
};

let randomizerMixin = Base => class extends Base {
  randomize() { }
};

ミックスインを使用したクラスを次のように記述することもできます:

class Foo { }
class Bar extends calculatorMixin(randomizerMixin(Foo)) { }

仕様

ブラウザー実装状況

BCD tables only load in the browser

クラス定義の再実行

クラスを再定義することはできません。再定義しようとすると SyntaxError が発生します。

Firefox のウェブコンソール(メニュー > ウェブ開発 > ウェブコンソール)などでコードを試しているときに、同じ名前のクラス定義を 2 回実行すると、SyntaxError: redeclaration of let ClassName が発生します。(この問題については バグ 1428672 でさらに詳しく説明しています。)Chrome Developer Tools で同様の操作を行うと、Uncaught SyntaxError: Identifier 'ClassName' has already been declared at <anonymous>:1:1 のようなメッセージが表示されます。

関連情報