15. クラス
目次
この書籍を応援してください: 購入する(PDF、EPUB、MOBI) または 寄付する
(広告、ブロックしないでください。)

15. クラス



15.1 概要

クラスとサブクラス

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return `(${this.x}, ${this.y})`;
    }
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y);
        this.color = color;
    }
    toString() {
        return super.toString() + ' in ' + this.color;
    }
}

クラスの使用

> const cp = new ColorPoint(25, 8, 'green');

> cp.toString();
'(25, 8) in green'

> cp instanceof ColorPoint
true
> cp instanceof Point
true

内部的には、ES6のクラスは根本的に新しいものではありません。主に、昔ながらのコンストラクタ関数を作成するためのより便利な構文を提供します。typeofを使用すると、それがわかります。

> typeof Point
'function'

15.2 基本

15.2.1 基底クラス

クラスはECMAScript 6でこのように定義されます。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return `(${this.x}, ${this.y})`;
    }
}

このクラスは、ES5のコンストラクタ関数と同じように使用します。

> var p = new Point(25, 8);
> p.toString()
'(25, 8)'

実際、クラス定義の結果は関数です。

> typeof Point
'function'

ただし、クラスは関数呼び出しではなく、newを介してのみ呼び出すことができます(この背後にある根拠は後で説明します)。

> Point()
TypeError: Classes can’t be function-called
15.2.1.1 クラス定義のメンバー間の区切り文字なし

クラス定義のメンバー間に区切り文字はありません。たとえば、オブジェクトリテラルのメンバーはカンマで区切られますが、これはクラス定義のトップレベルでは違法です。セミコロンは許可されていますが、無視されます。

class MyClass {
    foo() {}
    ; // OK, ignored
    , // SyntaxError
    bar() {}
}

セミコロンは、セミコロンで終了するメンバーを含む可能性のある将来の構文に備えて許可されています。カンマは、クラス定義がオブジェクトリテラルとは異なることを強調するために禁止されています。

15.2.1.2 クラス宣言は巻き上げられない

関数宣言は巻き上げられます:スコープに入ると、その中で宣言された関数は、宣言がどこで行われたかに関係なく、すぐに利用可能になります。つまり、後で宣言された関数を呼び出すことができます。

foo(); // works, because `foo` is hoisted

function foo() {}

対照的に、クラス宣言は巻き上げられません。したがって、クラスは、実行がその定義に到達して評価された後にのみ存在します。事前にアクセスすると、ReferenceErrorが発生します。

new Foo(); // ReferenceError

class Foo {}

この制限の理由は、クラスが、任意の値を持つextends句を持つことができるためです。その式は、適切な「場所」で評価する必要があり、その評価を巻き上げることはできません。

巻き上げがないことは、あなたが思うほど制限的ではありません。たとえば、クラス宣言の前にある関数は、そのクラスを参照できますが、関数を呼び出す前にクラス宣言が評価されるまで待つ必要があります。

function functionThatUsesBar() {
    new Bar();
}

functionThatUsesBar(); // ReferenceError
class Bar {}
functionThatUsesBar(); // OK
15.2.1.3 クラス式

関数と同様に、クラス定義には2つの種類、つまりクラスを定義する2つの方法があります。クラス宣言クラス式です。

関数式と同様に、クラス式は匿名にできます。

const MyClass = class {
    ···
};
const inst = new MyClass();

また、関数式と同様に、クラス式には内部でのみ表示される名前を付けることができます。

const MyClass = class Me {
    getClassName() {
        return Me.name;
    }
};
const inst = new MyClass();

console.log(inst.getClassName()); // Me
console.log(Me.name); // ReferenceError: Me is not defined

最後の2行は、Meがクラス外の変数にならないが、内部で使用できることを示しています。

15.2.2 クラス定義の内部

クラス本体にはメソッドのみを含めることができますが、データプロパティを含めることはできません。データプロパティを持つプロトタイプは、一般的にアンチパターンと見なされているため、これはベストプラクティスを強制するだけです。

15.2.2.1 constructor、静的メソッド、プロトタイプメソッド

クラス定義でよく見られる3種類のメソッドを見てみましょう。

class Foo {
    constructor(prop) {
        this.prop = prop;
    }
    static staticMethod() {
        return 'classy';
    }
    prototypeMethod() {
        return 'prototypical';
    }
}
const foo = new Foo(123);

このクラス宣言のオブジェクト図は次のようになります。理解するためのヒント:[[Prototype]]はオブジェクト間の継承関係であり、prototypeは値がオブジェクトである通常のプロパティです。プロパティprototypeは、作成するインスタンスのプロトタイプとしてその値を使用するnew演算子に関してのみ特別です。

まず、擬似メソッドconstructorこのメソッドは特別であり、クラスを表す関数を定義します。

> Foo === Foo.prototype.constructor
true
> typeof Foo
'function'

これはクラスコンストラクタと呼ばれることもあります。通常のコンストラクタ関数にはない機能があります(主に、super()を介してスーパークラスのコンストラクタを呼び出す機能、これについては後で説明します)。

次に、静的メソッド。静的プロパティ(またはクラスプロパティ)は、Foo自体のプロパティです。メソッド定義の前にstaticを付けると、クラスメソッドが作成されます。

> typeof Foo.staticMethod
'function'
> Foo.staticMethod()
'classy'

3つ目は、プロトタイプメソッド。Fooプロトタイププロパティは、Foo.prototypeのプロパティです。それらは通常メソッドであり、Fooのインスタンスによって継承されます。

> typeof Foo.prototype.prototypeMethod
'function'
> foo.prototypeMethod()
'prototypical'
15.2.2.2 静的データプロパティ

ES6クラスを時間内に完成させるために、それらは意図的に「最大限に最小限」になるように設計されました。そのため、現在では静的メソッド、ゲッター、セッターのみを作成できますが、静的データプロパティを作成することはできません。言語に追加するための提案があります。その提案が受け入れられるまで、使用できる2つの回避策があります。

まず、静的プロパティを手動で追加できます。

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

Object.defineProperty()を使用して読み取り専用プロパティを作成できますが、割り当てのシンプルさが気に入っています。

次に、静的ゲッターを作成できます。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    static get ZERO() {
        return new Point(0, 0);
    }
}

どちらの場合も、読み取ることができるプロパティPoint.ZEROを取得します。最初の場合は、毎回同じインスタンスが返されます。2番目の場合は、毎回新しいインスタンスが返されます。

15.2.2.3 ゲッターとセッター

ゲッターとセッターの構文は、ECMAScript 5のオブジェクトリテラルとまったく同じです。

class MyClass {
    get prop() {
        return 'getter';
    }
    set prop(value) {
        console.log('setter: '+value);
    }
}

MyClassは次のように使用します。

> const inst = new MyClass();
> inst.prop = 123;
setter: 123
> inst.prop
'getter'
15.2.2.4 計算されたメソッド名

式を角括弧に入れると、式を介してメソッドの名前を定義できます。たとえば、次のFooの定義方法はすべて同等です。

class Foo {
    myMethod() {}
}

class Foo {
    ['my'+'Method']() {}
}

const m = 'myMethod';
class Foo {
    [m]() {}
}

ECMAScript 6のいくつかの特殊なメソッドには、シンボルであるキーがあります。計算されたメソッド名を使用すると、そのようなメソッドを定義できます。たとえば、オブジェクトにキーがSymbol.iteratorであるメソッドがある場合、それは反復可能です。つまり、その内容はfor-ofループや他の言語メカニズムで反復処理できます。

class IterableClass {
    [Symbol.iterator]() {
        ···
    }
}
15.2.2.5 ジェネレーターメソッド

メソッド定義の前にアスタリスク(*)を付けると、それはジェネレーターメソッドになります。とりわけ、ジェネレーターは、キーがSymbol.iteratorであるメソッドを定義するのに役立ちます。次のコードは、それがどのように機能するかを示しています。

class IterableArguments {
    constructor(...args) {
        this.args = args;
    }
    * [Symbol.iterator]() {
        for (const arg of this.args) {
            yield arg;
        }
    }
}

for (const x of new IterableArguments('hello', 'world')) {
    console.log(x);
}

// Output:
// hello
// world

15.2.3 サブクラス化

extends句を使用すると、既存のコンストラクタ(クラスを介して定義されたかどうかに関係なく)のサブクラスを作成できます。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return `(${this.x}, ${this.y})`;
    }
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y); // (A)
        this.color = color;
    }
    toString() {
        return super.toString() + ' in ' + this.color; // (B)
    }
}

繰り返しますが、このクラスは期待どおりに使用されます。

> const cp = new ColorPoint(25, 8, 'green');
> cp.toString()
'(25, 8) in green'

> cp instanceof ColorPoint
true
> cp instanceof Point
true

クラスには2種類あります。

superを使用する方法は2つあります。

15.2.3.1 サブクラスのプロトタイプはスーパークラスである

サブクラスのプロトタイプは、ECMAScript 6ではスーパークラスです。

> Object.getPrototypeOf(ColorPoint) === Point
true

つまり、静的プロパティが継承されます。

class Foo {
    static classMethod() {
        return 'hello';
    }
}

class Bar extends Foo {
}
Bar.classMethod(); // 'hello'

静的メソッドをスーパー呼び出しすることもできます。

class Foo {
    static classMethod() {
        return 'hello';
    }
}

class Bar extends Foo {
    static classMethod() {
        return super.classMethod() + ', too';
    }
}
Bar.classMethod(); // 'hello, too'
15.2.3.2 スーパークラスコンストラクタの呼び出し

派生クラスでは、thisを使用する前にsuper()を呼び出す必要があります。

class Foo {}

class Bar extends Foo {
    constructor(num) {
        const tmp = num * 2; // OK
        this.num = num; // ReferenceError
        super();
        this.num = num; // OK
    }
}

暗黙的にsuper()を呼び出さずに派生コンストラクタを離れると、エラーも発生します。

class Foo {}

class Bar extends Foo {
    constructor() {
    }
}

const bar = new Bar(); // ReferenceError
15.2.3.3 コンストラクタの結果の上書き

ES5と同様に、オブジェクトを明示的に返すことで、コンストラクタの結果を上書きできます。

class Foo {
    constructor() {
        return Object.create(null);
    }
}
console.log(new Foo() instanceof Foo); // false

そうする場合は、thisが初期化されているかどうかは関係ありません。つまり、この方法で結果を上書きする場合は、派生コンストラクタでsuper()を呼び出す必要はありません。

15.2.3.4 クラスのデフォルトコンストラクタ

基底クラスにconstructorを指定しない場合は、次の定義が使用されます。

constructor() {}

派生クラスの場合、次のデフォルトコンストラクタが使用されます。

constructor(...args) {
    super(...args);
}
15.2.3.5 組み込みコンストラクタのサブクラス化

ECMAScript 6では、最終的にすべての組み込みコンストラクタをサブクラス化できます(ES5には回避策がありますが、これらには重大な制限があります)。

たとえば、(ほとんどのエンジンでスタックトレースを持つという機能を継承する)独自の例外クラスを作成できるようになりました。

class MyError extends Error {
}
throw new MyError('Something happened!');

また、インスタンスがlengthを適切に処理するArrayのサブクラスを作成することもできます。

class Stack extends Array {
    get top() {
        return this[this.length - 1];
    }
}

var stack = new Stack();
stack.push('world');
stack.push('hello');
console.log(stack.top); // hello
console.log(stack.length); // 2

Arrayのサブクラス化は通常、最適なソリューションではないことに注意してください。多くの場合、独自のクラス(インターフェースを制御する)を作成し、プライベートプロパティでArrayに委任する方が優れています。

15.3 クラスのプライベートデータ

このセクションでは、ES6クラスのプライベートデータを管理するための4つのアプローチについて説明します。

  1. クラスのconstructorの環境にプライベートデータを保持する
  2. 命名規則(例:プレフィックスにアンダースコア)を介してプライベートプロパティをマークする
  3. WeakMapsにプライベートデータを保持する
  4. プライベートプロパティのキーとしてシンボルを使用する

アプローチ#1と#2は、コンストラクター向けにES5ですでに一般的でした。アプローチ#3と#4はES6で新しく導入されました。各アプローチを使用して、同じ例を4回実装してみましょう。

15.3.1 コンストラクター環境を介したプライベートデータ

実行例は、カウンター(初期値はcounter)がゼロに達するとコールバックactionを1回呼び出すクラスCountdownです。2つのパラメーターactioncounterは、プライベートデータとして保存する必要があります。

最初の実装では、クラスコンストラクターの環境actioncounterを保存します。環境とは、JavaScriptエンジンが、新しいスコープが入力されるたび(関数呼び出しやコンストラクター呼び出しなどを介して)に生成されるパラメーターとローカル変数を格納する内部データ構造のことです。以下がコードです。

class Countdown {
    constructor(counter, action) {
        Object.assign(this, {
            dec() {
                if (counter < 1) return;
                counter--;
                if (counter === 0) {
                    action();
                }
            }
        });
    }
}

Countdownの使用は次のようになります。

> const c = new Countdown(2, () => console.log('DONE'));
> c.dec();
> c.dec();
DONE

利点

欠点

この手法の詳細については、「Speaking JavaScript」の「コンストラクターの環境におけるプライベートデータ(クロックフォードプライバシーパターン)」を参照してください。

15.3.2 命名規則によるプライベートデータ

次のコードは、プレフィックスにアンダースコアを付けて名前がマークされたプロパティにプライベートデータを保持します。

class Countdown {
    constructor(counter, action) {
        this._counter = counter;
        this._action = action;
    }
    dec() {
        if (this._counter < 1) return;
        this._counter--;
        if (this._counter === 0) {
            this._action();
        }
    }
}

利点

欠点

15.3.3 WeakMapsによるプライベートデータ

WeakMapsを使用する優れた手法があり、最初のアプローチ(安全性)の利点と2番目のアプローチ(プロトタイプメソッドを使用できること)の利点を組み合わせています。この手法を次のコードで示します。プライベートデータを格納するために、WeakMaps _counter_actionを使用します。

const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
    constructor(counter, action) {
        _counter.set(this, counter);
        _action.set(this, action);
    }
    dec() {
        let counter = _counter.get(this);
        if (counter < 1) return;
        counter--;
        _counter.set(this, counter);
        if (counter === 0) {
            _action.get(this)();
        }
    }
}

2つのWeakMaps _counter_actionのそれぞれは、オブジェクトをそれぞれのプライベートデータにマッピングします。WeakMapsの仕組みにより、オブジェクトがガベージコレクションされるのを妨げることはありません。外部にWeakMapsを隠しておけば、プライベートデータは安全です。

さらに安全にするには、WeakMap.prototype.getWeakMap.prototype.setを変数に保存し、(メソッドではなく)それらを動的に呼び出すことができます。

const set = WeakMap.prototype.set;
···
set.call(_counter, this, counter);
    // _counter.set(this, counter);

そうすれば、悪意のあるコードがこれらのメソッドをプライベートデータを盗聴するようなメソッドに置き換えても、コードに影響はありません。ただし、保護されるのはコードの後に実行されるコードに対してのみです。自分のコードより前に実行される場合は、何もできません。

利点

欠点

15.3.4 シンボルによるプライベートデータ

プライベートデータの別の保存場所は、キーがシンボルであるプロパティです。

const _counter = Symbol('counter');
const _action = Symbol('action');

class Countdown {
    constructor(counter, action) {
        this[_counter] = counter;
        this[_action] = action;
    }
    dec() {
        if (this[_counter] < 1) return;
        this[_counter]--;
        if (this[_counter] === 0) {
            this[_action]();
        }
    }
}

各シンボルは一意であるため、シンボル値のプロパティキーが他のプロパティキーと衝突することはありません。さらに、シンボルは外部からはいくらか隠されていますが、完全ではありません。

const c = new Countdown(2, () => console.log('DONE'));

console.log(Object.keys(c));
    // []
console.log(Reflect.ownKeys(c));
    // [ Symbol(counter), Symbol(action) ]

利点

欠点

15.3.5 参考文献

15.4 シンプルなミックスイン

JavaScriptでのサブクラス化は、2つの理由で使用されます。

実装継承のためのクラスの有用性は限定的です。なぜなら、単一継承しかサポートしていないからです(クラスには最大1つのスーパークラスを持つことができます)。そのため、複数のソースからツールメソッドを継承することはできません。それらはすべてスーパークラスから取得する必要があります。

では、どうすればこの問題を解決できるでしょうか?例を使って解決策を探ってみましょう。EmployeePersonのサブクラスであるエンタープライズの管理システムを考えてみましょう。

class Person { ··· }
class Employee extends Person { ··· }

さらに、ストレージとデータ検証のためのツールクラスがあります。

class Storage {
    save(database) { ··· }
}
class Validation {
    validate(schema) { ··· }
}

次のようにツールクラスを含めることができると便利です。

// Invented ES6 syntax:
class Employee extends Storage, Validation, Person { ··· }

つまり、EmployeeStorageのサブクラスであり、それがValidationのサブクラスであり、それがPersonのサブクラスであるようにする必要があります。EmployeePersonは、そのようなクラスのチェーンで1回だけ使用されます。しかし、StorageValidationは複数回使用されます。それらを、スーパークラスを埋めるクラスのテンプレートとして使用したいのです。そのようなテンプレートは、抽象サブクラスまたはミックスインと呼ばれます。

ES6でミックスインを実装する1つの方法は、入力をスーパークラスとし、出力をそのスーパークラスを拡張するサブクラスとする関数と見なすことです。

const Storage = Sup => class extends Sup {
    save(database) { ··· }
};
const Validation = Sup => class extends Sup {
    validate(schema) { ··· }
};

ここでは、extends句のオペランドが固定識別子ではなく、任意の式であるという利点を活用しています。これらのミックスインを使用すると、Employeeは次のように作成されます。

class Employee extends Storage(Validation(Person)) { ··· }

謝辞。私が知る限り、この手法が最初に登場したのは、Sebastian MarkbågeによるGistです。

15.5 クラスの詳細

これまで見てきたのは、クラスの要点です。内部で何が起こっているかに興味がある場合にのみ、読み進めてください。クラスの構文から始めましょう。以下は、ECMAScript 6仕様のセクションA.4に示されている構文をわずかに変更したものです。

ClassDeclaration:
    "class" BindingIdentifier ClassTail
ClassExpression:
    "class" BindingIdentifier? ClassTail

ClassTail:
    ClassHeritage? "{" ClassBody? "}"
ClassHeritage:
    "extends" AssignmentExpression
ClassBody:
    ClassElement+
ClassElement:
    MethodDefinition
    "static" MethodDefinition
    ";"

MethodDefinition:
    PropName "(" FormalParams ")" "{" FuncBody "}"
    "*" PropName "(" FormalParams ")" "{" GeneratorBody "}"
    "get" PropName "(" ")" "{" FuncBody "}"
    "set" PropName "(" PropSetParams ")" "{" FuncBody "}"

PropertyName:
    LiteralPropertyName
    ComputedPropertyName
LiteralPropertyName:
    IdentifierName  /* foo */
    StringLiteral   /* "foo" */
    NumericLiteral  /* 123.45, 0xFF */
ComputedPropertyName:
    "[" Expression "]"

2つの注目点

15.5.1 さまざまなチェック

15.5.2 プロパティの属性

クラス宣言は、(変更可能な)letバインディングを作成します。次の表は、特定のクラスFooに関連するプロパティの属性を示しています。

  書き込み可能 列挙可能 構成可能
静的プロパティFoo.* true false true
true false false false
true false false true
プロトタイププロパティFoo.prototype.* true false true

注釈

15.5.3 クラスには内部名がある

クラスには、名前付き関数式と同様に、レキシカル内部名があります。

15.5.3.1 名前付き関数式の内部名

名前付き関数式には、レキシカル内部名があることをご存知かもしれません。

const fac = function me(n) {
    if (n > 0) {
        // Use inner name `me` to
        // refer to function
        return n * me(n-1);
    } else {
        return 1;
    }
};
console.log(fac(3)); // 6

名前付き関数式の名前meは、現在関数を保持している変数に影響されない、レキシカルにバインドされた変数になります。

15.5.3.2 クラスの内部名

興味深いことに、ES6クラスには、メソッド(コンストラクターメソッドと通常のメソッド)で使用できるレキシカル内部名もあります。

class C {
    constructor() {
        // Use inner name C to refer to class
        console.log(`constructor: ${C.prop}`);
    }
    logProp() {
        // Use inner name C to refer to class
        console.log(`logProp: ${C.prop}`);
    }
}
C.prop = 'Hi!';

const D = C;
C = null;

// C is not a class, anymore:
new C().logProp();
    // TypeError: C is not a function

// But inside the class, the identifier C
// still works
new D().logProp();
    // constructor: Hi!
    // logProp: Hi!

(ES6仕様では、内部名はClassDefinitionEvaluationの動的セマンティクスによって設定されます。)

謝辞:クラスに内部名があることを指摘してくれたMichael Ficarraに感謝します。

15.6 サブクラス化の詳細

ECMAScript 6では、サブクラス化は次のようになります。

class Person {
    constructor(name) {
        this.name = name;
    }
    toString() {
        return `Person named ${this.name}`;
    }
    static logNames(persons) {
        for (const person of persons) {
            console.log(person.name);
        }
    }
}

class Employee extends Person {
    constructor(name, title) {
        super(name);
        this.title = title;
    }
    toString() {
        return `${super.toString()} (${this.title})`;
    }
}

const jane = new Employee('Jane', 'CTO');
console.log(jane.toString()); // Person named Jane (CTO)

次のセクションでは、前の例で作成されたオブジェクトの構造を調べます。その後のセクションでは、janeがどのように割り当てられて初期化されるかを調べます。

15.6.1 プロトタイプチェーン

前の例では、次のオブジェクトが作成されます。

プロトタイプチェーンとは、[[Prototype]]関係(これは継承関係)を介してリンクされたオブジェクトです。図では、2つのプロトタイプチェーンを見ることができます。

15.6.1.1 左側の列:クラス(関数)

派生クラスのプロトタイプは、それが拡張するクラスです。この設定の理由は、サブクラスがスーパークラスのすべてのプロパティを継承するようにしたいからです。

> Employee.logNames === Person.logNames
true

基本クラスのプロトタイプは、Function.prototypeであり、これも関数のプロトタイプです。

> const getProto = Object.getPrototypeOf.bind(Object);

> getProto(Person) === Function.prototype
true
> getProto(function () {}) === Function.prototype
true

つまり、基本クラスと、それらから派生したすべてのクラス(それらのプロトタイプ)は関数です。従来のES5関数は、本質的に基本クラスです。

15.6.1.2 右側の列:インスタンスのプロトタイプチェーン

クラスの主な目的は、このプロトタイプチェーンを構築することです。プロトタイプチェーンはObject.prototype(そのプロトタイプはnull)で終わります。これにより、Objectは(インスタンスとinstanceof演算子に関する限り)すべての基本クラスの暗黙のスーパークラスとなります。

この設定の理由は、サブクラスのインスタンスプロトタイプに、スーパークラスのインスタンスプロパティをすべて継承させたいからです。

ちなみに、オブジェクトリテラルで作成されたオブジェクトも、プロトタイプとしてObject.prototypeを持っています。

> Object.getPrototypeOf({}) === Object.prototype
true

15.6.2 インスタンスの割り当てと初期化

クラスコンストラクタ間のデータフローは、ES5でのサブクラス化の標準的な方法とは異なります。内部的には、おおよそ次のようになります。

// Base class: this is where the instance is allocated
function Person(name) {
    // Performed before entering this constructor:
    this = Object.create(new.target.prototype);

    this.name = name;
}
···

function Employee(name, title) {
    // Performed before entering this constructor:
    this = uninitialized;

    this = Reflect.construct(Person, [name], new.target); // (A)
        // super(name);

    this.title = title;
}
Object.setPrototypeOf(Employee, Person);
···

const jane = Reflect.construct( // (B)
             Employee, ['Jane', 'CTO'],
             Employee);
    // const jane = new Employee('Jane', 'CTO')

インスタンスオブジェクトは、ES6とES5では異なる場所で作成されます。

前のコードでは、2つの新しいES6機能を使用しています。

このサブクラス化方法の利点は、通常のコードで組み込みコンストラクタ(ErrorArrayなど)をサブクラス化できることです。後述のセクションで、なぜ異なるアプローチが必要だったのかを説明します。

念のため、ES5でのサブクラス化の方法は次のとおりです。

function Person(name) {
    this.name = name;
}
···

function Employee(name, title) {
    Person.call(this, name);
    this.title = title;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
···
15.6.2.1 安全チェック
15.6.2.2 extends

extends句がクラスの設定にどのように影響するかを調べましょう(仕様のセクション14.5.14)。

extends句の値は、「構築可能」(newを介して呼び出し可能)である必要があります。ただし、nullは許可されています。

class C {
}
class C extends B {
}
class C extends Object {
}

最初のケースとのわずかな違いに注意してください。extends句がない場合、クラスは基本クラスであり、インスタンスを割り当てます。クラスがObjectを拡張する場合、それは派生クラスであり、Objectがインスタンスを割り当てます。結果のインスタンス(およびそのプロトタイプチェーン)は同じですが、到達方法が異なります。

class C extends null {
}

このようなクラスを使用すると、プロトタイプチェーンでObject.prototypeを回避できます。

15.6.3 なぜES5で組み込みコンストラクタをサブクラス化できないのですか?

ECMAScript 5では、ほとんどの組み込みコンストラクタはサブクラス化できません(いくつかの回避策は存在します)。

その理由を理解するために、標準的なES5パターンを使用してArrayをサブクラス化してみましょう。すぐにわかるように、これはうまくいきません。

function MyArray(len) {
    Array.call(this, len); // (A)
}
MyArray.prototype = Object.create(Array.prototype);

残念ながら、MyArrayをインスタンス化すると、正しく機能しないことがわかります。インスタンスプロパティlengthは、配列要素を追加しても反応して変化しません。

> var myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
0

myArrが適切な配列になるのを妨げる2つの障害があります。

最初の障害:初期化。(A行で)コンストラクタArrayに渡すthisは完全に無視されます。つまり、Arrayを使用してMyArray用に作成されたインスタンスを設定することはできません。

> var a = [];
> var b = Array.call(a, 3);
> a !== b  // a is ignored, b is a new object
true
> b.length // set up correctly
3
> a.length // unchanged
0

2番目の障害:割り当て。Arrayによって作成されるインスタンスオブジェクトはエキゾチックです(通常のオブジェクトにはない機能を持つオブジェクトに対するECMAScript仕様で使用される用語)。それらのプロパティlengthは、配列要素の管理を追跡および影響を与えます。一般に、エキゾチックオブジェクトは最初から作成できますが、既存の通常のオブジェクトをエキゾチックオブジェクトに変換することはできません。残念ながら、これはArrayがA行で呼び出された場合に実行する必要があることです。MyArray用に作成された通常のオブジェクトをエキゾチックな配列オブジェクトに変換する必要があります。

15.6.3.1 解決策:ES6サブクラス化

ECMAScript 6では、Arrayのサブクラス化は次のようになります。

class MyArray extends Array {
    constructor(len) {
        super(len);
    }
}

これは機能します。

> const myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
1

ES6のサブクラス化へのアプローチが、前述の障害をどのように解消するかを調べてみましょう。

15.6.4 メソッドでのスーパープロパティの参照

次のES6コードは、B行でスーパメソッド呼び出しを行っています。

class Person {
    constructor(name) {
        this.name = name;
    }
    toString() { // (A)
        return `Person named ${this.name}`;
    }
}

class Employee extends Person {
    constructor(name, title) {
        super(name);
        this.title = title;
    }
    toString() {
        return `${super.toString()} (${this.title})`; // (B)
    }
}

const jane = new Employee('Jane', 'CTO');
console.log(jane.toString()); // Person named Jane (CTO)

スーパー呼び出しがどのように機能するかを理解するために、janeのオブジェクト図を見てみましょう。

B行で、Employee.prototype.toStringは(A行で始まる)オーバーライドしたメソッドへのスーパー呼び出し(B行)を行います。メソッドが格納されているオブジェクトを、そのメソッドのホームオブジェクトと呼びましょう。たとえば、Employee.prototypeEmployee.prototype.toString()のホームオブジェクトです。

B行のスーパー呼び出しには、3つのステップが含まれます。

  1. 現在のメソッドのホームオブジェクトのプロトタイプで検索を開始します。
  2. 名前がtoStringであるメソッドを探します。そのメソッドは、検索が開始されたオブジェクト、またはプロトタイプチェーンの後のオブジェクトで見つかる可能性があります。
  3. 現在のthisを使用してそのメソッドを呼び出します。そうする理由は、スーパー呼び出しされたメソッドが同じインスタンスプロパティ(この例では、janeの独自のプロパティ)にアクセスできる必要があるためです。

スーパープロパティの取得(super.prop)または設定(super.prop = 123)のみを行っている場合でも(メソッド呼び出しではなく)、ゲッターまたはセッターが呼び出される可能性があるため、ステップ#3でthisが(内部的に)役割を果たす可能性があります。

これらのステップを3つの異なる方法(ただし、同等)で表現してみましょう。

// Variation 1: supermethod calls in ES5
var result = Person.prototype.toString.call(this) // steps 1,2,3

// Variation 2: ES5, refactored
var superObject = Person.prototype; // step 1
var superMethod = superObject.toString; // step 2
var result = superMethod.call(this) // step 3

// Variation 3: ES6
var homeObject = Employee.prototype;
var superObject = Object.getPrototypeOf(homeObject); // step 1
var superMethod = superObject.toString; // step 2
var result = superMethod.call(this) // step 3

バリエーション3は、ECMAScript 6がスーパー呼び出しを処理する方法です。このアプローチは、関数の環境が持つ2つの内部バインディングによってサポートされています(環境はスコープ内の変数のためのいわゆるバインディングというストレージスペースを提供します)。

15.6.4.1 superはどこで使用できますか?

プロトタイプチェーンが関係する場合はいつでもスーパープロパティを参照すると便利です。そのため、オブジェクトリテラルとクラス定義内のメソッド定義(ジェネレータメソッド定義、ゲッターとセッターを含む)で使用できます。クラスは派生しているかどうか、メソッドは静的であるかどうかは関係ありません。

プロパティを参照するためにsuperを使用することは、関数宣言、関数式、ジェネレータ関数では許可されていません。

15.6.4.2 落とし穴:superを使用するメソッドは移動できません

superを使用するメソッドは移動できません。このようなメソッドは、作成されたオブジェクトに結び付けられる内部スロット[[HomeObject]]を持っています。代入を介して移動した場合でも、元のオブジェクトのスーパープロパティを参照し続けます。将来のECMAScriptバージョンでは、そのようなメソッドも転送できる方法があるかもしれません。

15.7 種パターン

ECMAScript 6では、組み込みコンストラクタのもう1つのメカニズムが拡張可能になりました。メソッドがクラスの新しいインスタンスを作成する場合があります。サブクラスを作成した場合、メソッドは自分のクラスのインスタンスを返す必要がありますか、それともサブクラスのインスタンスを返す必要がありますか?いくつかの組み込みのES6メソッドでは、いわゆる種パターンを介してインスタンスを作成する方法を構成できます。

例として、Array のサブクラスである SortedArray を考えてみましょう。このクラスのインスタンスに対して map() を呼び出した場合、不要なソートを避けるために、Array のインスタンスを返すようにしたいとします。デフォルトでは、map() はレシーバー (this) のインスタンスを返しますが、種 (species) パターンを使用すると、それを変更できます。

15.7.1 例で使用するヘルパーメソッド

次の 3 つのセクションでは、例の中で 2 つのヘルパー関数を使用します。

function isObject(value) {
    return (value !== null
       && (typeof value === 'object'
           || typeof value === 'function'));
}

/**
 * Spec-internal operation that determines whether `x`
 * can be used as a constructor.
 */
function isConstructor(x) {
    ···
}

15.7.2 標準の種パターン

標準の種パターンは、Promise.prototype.then()、Typed Array の filter() メソッド、およびその他の操作で使用されます。その仕組みは次のとおりです。

JavaScript で実装すると、このパターンは次のようになります。

function SpeciesConstructor(O, defaultConstructor) {
    const C = O.constructor;
    if (C === undefined) {
        return defaultConstructor;
    }
    if (! isObject(C)) {
        throw new TypeError();
    }
    const S = C[Symbol.species];
    if (S === undefined || S === null) {
        return defaultConstructor;
    }
    if (! isConstructor(S)) {
        throw new TypeError();
    }
    return S;
}

15.7.3 配列の種パターン

通常の配列は、種パターンをわずかに異なる方法で実装します。

function ArraySpeciesCreate(self, length) {
    let C = undefined;
    // If the receiver `self` is an Array,
    // we use the species pattern
    if (Array.isArray(self)) {
        C = self.constructor;
        if (isObject(C)) {
            C = C[Symbol.species];
        }
    }
    // Either `self` is not an Array or the species
    // pattern didn’t work out:
    // create and return an Array
    if (C === undefined || C === null) {
        return new Array(length);
    }
    if (! IsConstructor(C)) {
        throw new TypeError();
    }
    return new C(length);
}

Array.prototype.map() は、ArraySpeciesCreate(this, this.length) を介して返す配列を作成します。

15.7.4 静的メソッドにおける種パターン

Promise は、Promise.all() などの静的メソッドに対して種パターンの変形を使用します。

let C = this; // default
if (! isObject(C)) {
    throw new TypeError();
}
// The default can be overridden via the property `C[Symbol.species]`
const S = C[Symbol.species];
if (S !== undefined && S !== null) {
    C = S;
}
if (!IsConstructor(C)) {
    throw new TypeError();
}
const instance = new C(···);

15.7.5 サブクラスでのデフォルトの種のオーバーライド

これは、プロパティ [Symbol.species] のデフォルトのゲッターです。

static get [Symbol.species]() {
    return this;
}

このデフォルトのゲッターは、組み込みクラスの ArrayArrayBufferMapPromiseRegExpSet、および %TypedArray% によって実装されます。これらは、これらの組み込みクラスのサブクラスによって自動的に継承されます。

デフォルトの種をオーバーライドするには、2 つの方法があります。任意のコンストラクターを使用するか、null を使用するかです。

15.7.5.1 任意のコンストラクターへの種のセット

静的ゲッターを介してデフォルトの種をオーバーライドできます (A 行)。

class MyArray1 extends Array {
    static get [Symbol.species]() { // (A)
        return Array;
    }
}

結果として、map()Array のインスタンスを返します。

const result1 = new MyArray1().map(x => x);
console.log(result1 instanceof Array); // true

デフォルトの種をオーバーライドしない場合、map() はサブクラスのインスタンスを返します。

class MyArray2 extends Array { }

const result2 = new MyArray2().map(x => x);
console.log(result2 instanceof MyArray2); // true
15.7.5.1.1 データプロパティによる種の指定

静的ゲッターを使用しない場合は、Object.defineProperty() を使用する必要があります。代入を使用することはできません。これは、ゲッターのみを持つキーを持つプロパティがすでに存在するためです。つまり、それは読み取り専用であり、代入することはできません。

たとえば、ここでは MyArray1 の種を Array に設定しています。

Object.defineProperty(
    MyArray1, Symbol.species, {
        value: Array
    });
15.7.5.2 種を null に設定する

種を null に設定すると、デフォルトのコンストラクターが使用されます (どちらのコンストラクターが使用されるかは、使用される種パターンのバリアントによって異なります。詳細については、前のセクションを参照してください)。

class MyArray3 extends Array {
    static get [Symbol.species]() {
        return null;
    }
}

const result3 = new MyArray3().map(x => x);
console.log(result3 instanceof Array); // true

15.8 クラスの長所と短所

クラスは JavaScript コミュニティ内で論争の的となっています。一方では、クラスベースの言語から来た人々は、JavaScript の型破りな継承メカニズムに対処する必要がなくなったことを喜んでいます。他方では、多くの JavaScript プログラマーは、JavaScript で複雑なのはプロトタイプ継承ではなく、コンストラクターだと主張しています。

ES6 クラスには、いくつかの明確な利点があります。

ES6 クラスに対するいくつかの一般的な不満を見てみましょう。私はそれらのほとんどに同意しますが、クラスの利点はその欠点をはるかに上回るとも思っています。私はそれらが ES6 に含まれていることをうれしく思っており、それらを使用することをお勧めします。

15.8.1 不満: ES6 クラスは JavaScript 継承の真の性質を曖昧にする

はい、ES6 クラスは JavaScript 継承の真の性質を曖昧にします。クラスの外観 (その構文) と動作 (そのセマンティクス) の間には、不幸な不一致があります。それはオブジェクトのように見えますが、関数です。私の好みでは、クラスはコンストラクター関数ではなく、コンストラクターオブジェクトであるべきでした。私は、Proto.js プロジェクト で、小さなライブラリ (このアプローチがどれほど適合しているかを証明するもの) を介して、そのアプローチを探ります。

ただし、下位互換性が重要であるため、クラスがコンストラクター関数であることも理にかなっています。そうすることで、ES6 コードと ES5 はより相互運用可能になります。

構文とセマンティクスの不一致は、ES6 以降でいくらかの摩擦を引き起こすでしょう。しかし、ES6 クラスを額面通りに受け取るだけで、快適な生活を送ることができます。私は、その幻想があなたを苦しめることはないと思います。初心者はより早く始めることができ、後で (言語に慣れてから) 舞台裏で何が起こっているのかを読むことができます。

15.8.2 不満: クラスは単一継承のみを提供する

クラスは単一継承のみを提供するため、オブジェクト指向設計に関して表現の自由を著しく制限します。ただし、それらは常にトレイトなどの多重継承メカニズムの基盤となるように計画されてきました。

すると、クラスはインスタンス化可能なエンティティになり、トレイトを組み立てる場所になります。それが起こるまでは、多重継承が必要な場合は、ライブラリに頼る必要があります。

15.8.3 不満: クラスは必須の new のためにあなたを閉じ込める

クラスをインスタンス化する場合は、ES6 で new を使用する必要があります。つまり、呼び出し側を変更せずにクラスからファクトリ関数に切り替えることはできません。これは確かに制限事項ですが、2 つの緩和要因があります。

したがって、クラスは構文的には ある程度 あなたを制限しますが、JavaScript にトレイトが導入されると、(オブジェクト指向設計に関して) 概念的 にはあなたを制限しません。

15.9 FAQ: クラス

15.9.1 なぜクラスを関数呼び出しできないのですか?

現在、クラスの関数呼び出しは禁止されています。これは、将来、クラスを介して関数呼び出しを処理する方法を追加するためのオプションをオープンにしておくために行われました。

15.9.2 引数の配列が与えられた場合、クラスをどのようにインスタンス化しますか?

クラスに対する Function.prototype.apply() の類似物は何ですか? つまり、クラス TheClass と引数の配列 args がある場合、どのように TheClass をインスタンス化しますか?

その 1 つの方法は、スプレッド演算子 (...) を介することです。

function instantiate(TheClass, args) {
    return new TheClass(...args);
}

別のオプションは、Reflect.construct() を使用することです。

function instantiate(TheClass, args) {
    return Reflect.construct(TheClass, args);
}

15.10 クラスの次のステップは何ですか?

クラスの設計モットーは「最大限に最小限」でした。いくつかの高度な機能が議論されましたが、最終的には TC39 によって満場一致で受け入れられる設計を得るために破棄されました。

今後の ECMAScript バージョンでは、この最小限の設計を拡張できるようになりました。クラスは、トレイト (またはミックスイン)、値オブジェクト (コンテンツが同じ場合は異なるオブジェクトが等しいとみなされるオブジェクト)、および const クラス (不変のインスタンスを生成するクラス) などの機能の基盤を提供します。

15.11 さらに読む

次のドキュメントは、この章の重要な情報源です。

次へ: 16. モジュール