上一节都是基于 ES5 の特性を基に、クラス class のような動作を模倣して実装しましたが、これらの方法にはそれぞれ問題があり、継承を実現するコードも非常に冗長で混乱していることがわかります。したがって、ES6
で新たに導入された class
キーワードは、クラスを正式に定義する能力を持っています。これは実際にはシンタックスシュガーであり、背後で使用されているのは依然としてプロトタイプとコンストラクタの概念です。
クラス定義#
クラス宣言とクラス式の 2 つの定義方法があり、どちらも class
キーワードを使用します。
// クラス宣言
class Person {}
// クラス式
const Animal = class {}
関数式と同様に、クラス式は評価される前に参照することはできません。
ただし、違いは次のとおりです。
- 関数宣言は昇格されますが、クラス定義は昇格されません
- 関数は 関数スコープ に制限され、クラスは ブロックスコープ に制限されます。
// 関数宣言は昇格されますが、クラス定義は昇格されません*
console.log(FunctionExpression); // undefined
var FunctionExpression = function() {};
console.log(FunctionExpression); // function() {}
console.log(ClassExpression); // undefined
var ClassExpression = class {};
console.log(ClassExpression); // class {}
// 関数は関数スコープに制限され、クラスはブロックスコープに制限されます。
{
function FunctionDeclaration() {}
class ClassDeclaration {}
}
console.log(FunctionDeclaration); // FunctionDeclaration() {}
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
クラス構成#
クラスには以下のメソッドを含めることができますが、すべてが必須ではなく、空のクラス定義も有効です。
- コンストラクタ(constructor)
- ゲッターおよびセッター関数(get と set)
- 静的クラスメソッド(static)
- その他のインスタンスメソッド
デフォルトでは、クラス定義内のコードは 厳格モード で実行されます。
先頭の大文字は特に説明する必要はなく、インスタンスを区別するために使用できます。
クラス式の名前はオプションです。クラス式を変数に割り当てた後、name
プロパティを通じてクラス式の名前文字列を取得できます。ただし、クラス式のスコープ外でこの識別子にアクセスすることはできません。
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name);
}
}
let p = new Person();
p.identify(); // PersonName PersonName
console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined
クラスコンストラクタ#
constructor
キーワードは、クラス定義ブロック内でクラスのコンストラクタを作成するために使用されます。
constructor
はインタープリターに、new
演算子を使用してクラスの新しいインスタンスを作成する際に、この関数を呼び出すべきであることを伝えます。- コンストラクタの定義は必須ではなく、コンストラクタを定義しないことは、コンストラクタを空の関数 ** として定義することに相当します。
1、インスタンス化#
constructor 関数は実際にはシンタックスシュガーであり、JS インタープリターにnew
を使用してクラスのインスタンスを定義する際にconstructor
関数を使用してインスタンス化することを知らせます。
new
を使用してコンストラクタを呼び出す際に実行される操作を復習しましょう:
- メモリ内に新しいオブジェクトを作成します
let obj = new Object()
- 新しいオブジェクト内部の
[[Prototype]]
をコンストラクタのprototype
に割り当てますobj.__proto__ = constructor.prototype;
- コンストラクタ内部の
this
はこの新しいオブジェクトを指します - このコンストラクタ内部のコードを実行します
- 上記の 2 つのステップは
let res = contructor.apply(obj, args)
に相当します。
- 上記の 2 つのステップは
- コンストラクタが 非空オブジェクト を返す場合、そのオブジェクトを返します
res
;そうでなければ、作成された新しいオブジェクトobj
を返します。return typeof res === 'object' ? res: obj;
クラスインスタンス化時に渡される引数はコンストラクタの引数として使用され、引数が不要な場合は、クラス名の後の括弧もオプションで、直接 new Person
とすることができます。
クラスコンストラクタは実行後にオブジェクトを返し、このオブジェクトがインスタンス化されたオブジェクトとして使用されます。
注意:コンストラクタが返すこのオブジェクト res
が非空オブジェクトであり、このオブジェクトが new の最初のステップで新しく作成されたオブジェクト obj
と関係がない場合、新しく作成されたオブジェクトは回収され、instanceof
で検出してもそのクラスとの関連性は検出できません。なぜなら、プロトタイプポインタが変更されていないからです。
2、クラスの本質?#
class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function
これは関数です!前述のように、実質的にはシンタックスシュガーであり、関数と同じ動作を持っています。
他のオブジェクトや関数の参照と同様に、パラメータとして渡すことができ、即座にインスタンス化することもできます(関数式の即時実行に似ています)。
// クラスは関数のように任意の場所で定義できます。たとえば、配列の中で
let classList = [
class {
constructor(id) {
this.id_ = id;
console.log(`instance ${this.id_}`);
}
}
];
function createInstance(classDefinition, id) {
return new classDefinition(id);
}
let foo = createInstance(classList[0], 3141); // instance 3141
// 即時実行
let p = new class Foo {
constructor(x) {
console.log(x);
}
}('bar'); // bar
console.log(p); // Foo {}
インスタンス、プロトタイプとクラスメンバー#
クラスは非常に便利に以下の 3 種類のメンバーを定義できます。他の言語と同様です。
- インスタンス上のメンバー
- プロトタイプ上のメンバー
- クラス自体のメンバー(静的クラスメンバー)
インスタンスメンバー#
constructor
内で this を使用して新しく作成されたインスタンスに独自の属性を追加できます。各インスタンスはここでの属性を共有しません。
- ps:クラスブロック内に直接書かれたメンバーもインスタンス属性になります。
class Person {
sex = '女'
age = 21
constructor() {
// この例では、オブジェクトラッパータイプを使用して文字列を定義します。
// これは、以下で2つのオブジェクトの等価性をテストするためです。
this.name = new String('Jack');
this.sayName = () => console.log(this.name);
this.nicknames = ['Jake', 'J-Dog']
}
}
let p1 = new Person(),
p2 = new Person();
p1.sayName(); // Jack
p2.sayName(); // Jack
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nicknames === p2.nicknames); // false
console.log(p1.sex, p1.age) // 女 21
プロトタイプメンバー#
クラスブロック内で定義されたメソッドはプロトタイプメソッドとして扱われ(すべてのインスタンスで共有されます)。
class Person {
constructor() {
// thisに追加されたすべての内容は異なるインスタンスに存在します。
this.locate = () => console.log('instance');
}
// クラスブロック内で定義されたすべての内容はクラスのプロトタイプに定義されます。
locate() {
console.log('prototype');
}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
クラス定義はゲッターおよびセッターアクセサもサポートしています。構文と動作は通常のオブジェクトと同様です:
class Person {
sex = '女'
age = 21
constructor() {
this.sayName = () => console.log(this.name);
this.nicknames = ['Jake', 'J-Dog']
}
set name(newName) {
this.name_ = newName;
}
get name() {
return this.name_;
}
}
let p = new Person();
p.name = 'cosine';
console.log(p.name); // cosine
静的クラスメソッド#
静的クラスメンバーは、クラス定義内で static
キーワードをプレフィックスとして使用します。静的メンバー内では、this
はクラス自体を参照します。クラス名。メソッド名を介して直接アクセスできます。
非関数プロトタイプおよびクラスメンバー#
クラス定義は明示的にプロトタイプやクラスにメンバーデータを追加することをサポートしていませんが、クラス定義の外部で手動で追加できます:
// クラス上にデータメンバーを定義
Person.greeting = 'My name is';
// プロトタイプ上にデータメンバーを定義
Person.prototype.name = 'Jake';
- ただし、これは推奨されません。プロトタイプやクラスに可変データメンバーを追加することは反パターンです。一般的に、オブジェクトインスタンスは
this
を介して参照されるデータを独自に持つべきです。
イテレーターとジェネレーター方法#
クラス定義構文は、プロトタイプおよびクラス自体にジェネレーター方法を定義することをサポートしています:
class Person {
// プロトタイプ上にジェネレーター方法を定義
*createNicknameIterator() {
yield 'cosine1';
yield 'cosine2';
yield 'cosine3';
}
// クラス上にジェネレーター方法を定義
static *createJobIterator() {
yield 'bytedance';
yield 'mydream';
yield 'bytedance mydream!';
}
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // bytedance
console.log(jobIter.next().value); // mydream
console.log(jobIter.next().value); // bytedance mydream!
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // cosine1
console.log(nicknameIter.next().value); // cosine2
console.log(nicknameIter.next().value); // cosine3
ジェネレーター方法をサポートしているため、デフォルトのイテレーターを追加することで、クラスインスタンスをイテラブルオブジェクトに変えることができます。
class People {
constructor() {
this.nicknames = ['cosine1', 'cosine2', 'cosine3'];
}
*[Symbol.iterator]() {
yield *this.nicknames.entries();
}
}
let workers = new People();
for (let [idx, nickname] of workers) {
console.log(idx, nickname);
}
// 0 cosine1
// 1 cosine2
// 2 cosine3
継承#
ES 6 の最も優れた点は、クラス継承メカニズムをネイティブにサポートしていることであり、これは以前のプロトタイプチェーンのシンタックスシュガーです。
継承の基礎#
ES6 クラスは単一継承をサポートしており、extends
キーワードを使用して、クラスを継承したり、通常のコンストラクタを継承したりできます(後方互換性を維持)。
- 派生クラスはプロトタイプチェーンを介してクラスおよびプロトタイプ上で定義されたメソッドにアクセスできます。
this
の値は、対応するメソッドを呼び出すインスタンスまたはクラスを反映します。extends
キーワードはクラス式でも使用できます。
コンストラクタ、HomeObject と super ()#
- 派生クラスのメソッドは
super
キーワードを介してそのプロトタイプを参照できます。- 派生クラス内でのみ使用できます。
- クラスコンストラクタ、インスタンスメソッド、および静的メソッド内部に制限されます。
- クラスコンストラクタ内で
super
を使用すると、親クラスのコンストラクタを呼び出すことができます。
[[HomeObject]]
- ES6 はクラスコンストラクタと静的メソッドに内部特性
[[HomeObject]]
を追加しました。 - このポインタはそのメソッドを定義したオブジェクトを指し、このポインタは自動的に割り当てられ、JavaScript エンジン内部でのみアクセスできます。
- ES6 はクラスコンストラクタと静的メソッドに内部特性
super
は常に[[HomeObject]]
のプロトタイプとして定義されます。
super
を使用する際にはいくつかの問題に注意が必要です。
super
は 派生クラスのコンストラクタと静的メソッド内でのみ使用できます。super
キーワードを単独で参照することはできず、コンストラクタを呼び出すか、静的メソッドを参照する必要があります。super()
を呼び出すと、親クラスのコンストラクタが呼び出され、返されたインスタンスがthis
に割り当てられるため、super()
を呼び出す前にthis
を参照することはできません。- 親クラスのコンストラクタに引数を渡す必要がある場合は、手動でそれを
super
に渡す必要があります。 - クラスコンストラクタを定義していない場合、派生クラスをインスタンス化すると
super()
が呼び出され、すべての派生クラスに渡された引数が渡されます。 - 派生クラス内で明示的にコンストラクタを定義した場合は、その中で
super()
を呼び出すか、明示的にオブジェクトを返す必要があります。
抽象基類#
抽象基類とは、** 他のクラスが継承できるが、自体はインスタンス化されないクラスのことです。他の言語にもこの概念があります。ECMAScript はこの種のクラスの構文を特にサポートしていませんが、new.target
を使用することで簡単に実現できます。
new.target
はnew
キーワードを使用して呼び出されたクラスまたは関数を保持します。- インスタンス化時に
new.target
が抽象基類であるかどうかを検出することで、抽象基類のインスタンス化を防ぐことができます。
// 抽象基類
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
}
}
// 派生クラス
class Bus extends Vehicle {}
new Bus(); // class Bus {}
new Vehicle(); // class Vehicle {}
// Error: Vehicle cannot be directly instantiated
抽象基類のコンストラクタ内でチェックを行うことで、派生クラスが特定のメソッドを定義する必要があることを要求できます。プロトタイプメソッドは、プロトタイプクラスのコンストラクタを呼び出す前に既に存在しているため、this
キーワードを使用して対応するメソッドが定義されているかどうかを確認できます。
// 抽象基類
class Vehicle {
constructor() {
if (new.target === Vehicle)
throw new Error('Vehicle cannot be directly instantiated');
if (!this.foo)
throw new Error('Inheriting class must define foo()');
console.log('success!');
}
}
// 派生クラス
class Bus extends Vehicle { foo() {} }
// 派生クラス
class Van extends Vehicle {}
new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()
組み込み型の継承#
ES6 クラスは、組み込み参照型を継承するためのスムーズなメカニズムを提供し、開発者は組み込み型を簡単に拡張できます。以下は、Array にシャッフルアルゴリズムのメソッドを追加した例です。
class SuperArray extends Array {
shuffle() {
// シャッフルアルゴリズムを追加
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); // [0, i+1)のランダム数を生成
[this[i], this[j]] = [this[j], this[i]]; // 2つのカードを交換
}
}
}
let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
console.log(a); // [1, 2, 3, 4, 5]
a.shuffle();
console.log(a); // ランダムな新しい配列
a.shuffle();
console.log(a); // ランダムな新しい配列
一部の組み込み型のメソッドは新しいインスタンスを返します。デフォルトでは、返されるインスタンスの型は元のインスタンスの型と一致しますが、このデフォルトの動作を上書きしたい場合は、Symbol.species
アクセサを上書きすることができます。このアクセサは、返されるインスタンスを作成する際に使用されるクラスを決定します。
class SuperArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x%2))
console.log(a1); // [1, 2, 3, 4, 5]
console.log(a2); // [1, 3, 5]
console.log(a1 instanceof SuperArray, a1 instanceof Array); // true true
console.log(a2 instanceof SuperArray, a2 instanceof Array); // false true
クラスミックスイン(多重継承の模擬実装)#
異なるクラスの動作を 1 つのクラスに集中させることは、一般的な JavaScript のパターンです。ES6 は明示的に多重継承をサポートしていませんが、既存の機能を使用してこの動作を簡単に模倣できます。
まず、以下の 2 点に注意してください:
- もし複数のオブジェクトの属性をミックスインするだけであれば、
Object.assign()
を使用すればよいです。Object.assign()
メソッドは、オブジェクトの動作をミックスインするために特別に設計されています。クラスの動作をミックスインする必要がある場合にのみ、自分でミックスイン式を実装する必要があります。
- 多くの JavaScript フレームワーク(特に React)は、ミックスインパターンを放棄し、コンポジションパターンに移行しています。
- コンポジションとは、メソッドを独立したクラスやヘルパーオブジェクトに抽出し、それらを組み合わせることを意味し、継承を使用しません。
- よく知られているソフトウェア設計の原則は、**「コンポジションは継承に勝る(composition over inheritance)」** です。
extends
キーワードの後には JavaScript 式 を指定できます。クラスまたはコンストラクタに解決できる任意の式が有効です。この式はクラス定義が評価されるときに評価されます。これがクラスミックスインの原理です。
たとえば、Person クラスがクラス A、B、C を組み合わせる必要がある場合、B が A を継承し、C が B を継承し、Person が C を継承するというメカニズムを実装する必要があります。
class Vehicle {}
let AMixin = (Superclass) => class extends Superclass {
afunc() { console.log('A Mixin'); }
};
let BMixin = (Superclass) => class extends Superclass {
bfunc() { console.log('B Mixin'); }
};
let CMixin = (Superclass) => class extends Superclass {
cfunc() { console.log('C Mixin'); }
};
function mixin(BaseClass, ...Mixins) {
return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
class Bus extends mixin(Vehicle, AMixin, BMixin, CMixin) {}
let b = new Bus();
b.afunc(); // A Mixin
b.bfunc(); // B Mixin
b.cfunc(); // C Mixin
小結#
- ECMAScript 6 で新たに追加されたクラスは、既存のプロトタイプメカニズムに大きく基づいたシンタックスシュガーです。
- クラスの構文は、開発者が後方互換性のあるクラスを優雅に定義できるようにします。
- クラスは組み込み型を継承することも、カスタム型を継承することもできます。
- クラスはオブジェクトインスタンス、オブジェクトプロトタイプ、およびオブジェクトクラス間のギャップを効果的に埋めます。
- クラスミックスインを使用することで、多重継承に似た効果を巧妙に実現できますが、推奨されません。なぜなら「コンポジションは継承に勝る」からです。