0%

JS高程第4版新增章节翻译:CLASSES

下述内容翻译自 Professional JavaScript for Web Developes,4th Edition(JavaScript 高级程序设计第四版),251 页,Objects, Classes, and Object-Oriented Programming (对象、类和面向对象)章节内,前半部分与第三版的 138 页第 6 章面向对象的程序设计相似,所以主要翻译后半部分 302 页——CLASSES

类(CLASSES)

前面的部分是深入描述了如何使用 ECMAScript5 中可用的特性来模拟类(class)行为。可以发现存在各种问题和折中处理。除此之外,语法也过于冗长和混乱。

为了解决这些问题,ECMAScript6 中新引入了 class 关键字来正式定义类。类在 ECMAScript 中基本上是一个新的语法构造,因此起初可能会感到陌生。虽然 ECMAScript6 类似乎具有标准的面向对象程序设计,但是本质上仍然使用原型和构造函数的概念。

类定义基础

与函数类型类似,定义类有两种主要方式:类声明和类表达式。两者都使用了 class 关键字和大括号:

1
2
3
4
// 类声明
class Person {}
// 类表达式
const Animal = class {};

与函数表达式一样,只有在类表达式执行后才能被引用。然而,与函数定义的行为有一个重要的不同,函数声明会被提升,类声明不会提升:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log(FunctionExpression); // undefined
var FunctionExpression = function () {};
console.log(FunctionExpression); // function() {}

console.log(FunctionDeclaration); // FunctionDeclaration() {}
function FunctionDeclaration() {}
console.log(FunctionDeclaration); // FunctionDeclaration() {}

console.log(ClassExpression); // undefined
var ClassExpression = class {};
console.log(ClassExpression); // class {}

console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
class ClassDeclaration {}
console.log(ClassDeclaration); // class ClassDeclaration {}

与函数声明不同的是,类声明的作用域是块级:

1
2
3
4
5
6
{
function FunctionDeclaration() {}
class ClassDeclaration {}
}
console.log(FunctionDeclaration); // FunctionDeclaration() {}
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
类的组成

一个类可以由类构造函数方法、实例方法、getter、setter 和静态类方法组成。这些都不是显式必需的;空类定义也是有效的语法。默认情况下,类定义中的所有内容都以严格模式(strict mode)执行。

与函数式构造函数一样,类名一般大写,以便区分从类中创建的实例(例如,类 Foo{}可能创建一个实例 Foo):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 空类定义有效
class Foo {}
// 带有constructor的类定义有效
class Bar {
constructor() {}
}
// 带有getter的类定义有效
class Baz {
get myBaz() {}
}
// 带有静态方法的类定义有效
class Qux {
static myQux() {}
}

类表达式可以带有可选的命名。当表达式分配给变量时,可以使用 name 属性检索类表达式名称字符串,但是标识符本身不能超出类表达式作用域。

1
2
3
4
5
6
7
8
9
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 操作符创建一个新的实例。constructor 的定义是可选的。不定义类 constructor 和定义 constructor 为空函数是一样的。

初始化

使用 new 操作符实例化 一个 Person 类 的操作与实例化一个构造函数的操作相同。唯一可以感觉到的区别是 JavaScript 解释器明白在类中使用 new 意味着 constructor 应该用于实例化。

使用 new 调用类中的 constructor 会执行以下操作:

  • 在内存中创建一个新对象。
  • 新对象的内部[[Prototype]]指针被指定为 constructor 的 prototype 属性。
  • constructor 的 this 值被分配给新对象(因此当在 constructor 内部引用时,this 指向新对象)。
  • 执行 constructor 中的代码(将属性添加到新对象中)。
  • 如果 constructor 返回一个对象,则返回该对象。否则,将返回刚刚创建的新对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {}
class Person {
constructor() {
console.log('person ctor');
}
}
class Vegetable {
constructor() {
this.color = 'orange';
}
}
let a = new Animal();
let p = new Person(); // person ctor
let v = new Vegetable();
console.log(v.color); // orange

实例化类时提供的参数用作 constructor 的参数。如果不需要使用参数,实例化时类名后面的空括号是可选的:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
constructor(name) {
console.log(arguments.length);
this.name = name || null;
}
}
let p1 = new Person(); // 0
console.log(p1.name); // null
let p2 = new Person(); // 0
console.log(p2.name); // null
let p3 = new Person('Jake'); // 1
console.log(p3.name); // Jake

默认情况下,constructor 将在执行后返回 this 对象。如果从构造函数返回一个对象,该对象将被用作实例化对象,如果对该对象的引用没有保留,则新创建的该对象将被丢弃。但是,如果返回一个不同的对象,返回的对象将不会通过 instanceof 与类关联,因为新对象的原型指针从未被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
constructor(override) {
this.foo = 'foo';
if (override) {
return {
bar: 'bar',
};
}
}
}
let p1 = new Person(),
p2 = new Person(true);
console.log(p1); // Person{ foo: 'foo' }
console.log(p1 instanceof Person); // true
console.log(p2); // { bar: 'bar' }
console.log(p2 instanceof Person); // false

与构造函数的一个主要区别是,使用带有 constructor 的类时 new 运算符是强制性的。对于函数构造函数,当选择不使用 new 运算符时,构造函数将使用构造函数内部的全局 this 值(通常是 window 对象)。对于带有 constructor 的类,忽略 new 操作符会抛出一个错误:

1
2
3
4
5
6
function Person() {}
class Animal {}
// 构造函数直接调用时使用window作为this
let p = Person();
let a = Animal();
// TypeError: class constructor Animal cannot be invoked without 'new'

类的 constructor 方法并不特殊,在实例化之后,它的行为与常规实例方法相同(具有相同的构造函数限制)。正因为如此,可以在实例化后引用和使用它:

1
2
3
4
5
6
7
8
class Person {}
// 使用该类创建一个新的实例
let p1 = new Person();
p1.constructor();
// TypeError: Class constructor Person cannot be invoked without 'new'

// 使用指向类的constructor的引用创建一个新的实例
let p2 = new p1.constructor();
类是一个特殊函数

在 ECMAScript 规范中没有正式的类这个类型,而且 ECMAScript 类的行为在许多方面与特殊函数类似。一旦声明,当使用 typeof 操作符检查时,类标识符标识为一个函数:

1
2
3
class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function

类标识符有一个 prototype 属性,prototype 有一个引用类本身的 constructor 属性:

1
2
3
class Person {}
console.log(Person.prototype); // { constructor: f() }
console.log(Person === Person.prototype.constructor); // true

和函数构造函数一样,可以使用 instanceof 运算符来测试构造函数的原型是否出现在实例的原型链中:

1
2
3
class Person {}
let p = new Person();
console.log(p instanceof Person); // true

instanceof 操作符实际上是检查实例的原型链和构造函数,在这个例子中,构造函数将检查实例 p 和构造函数 Person,后者看起来是一个类。

如前所述,类的行为方式与构造函数相同,并且在类的上下文中,当 new 操作符应用到类时,类本身被认为是构造函数。重要的是,类定义中的 constructor 方法不被认为是构造函数,并且在与 instanceof 一起使用时将返回 false。但是如果 constructor 方法是直接调用的,就使用非类的构造函数是一样的,instanceof 结果就会相反:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {}

let p1 = new Person();

console.log(p1.constructor === Person); // true
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Person.constructor); // false

let p2 = new Person.constructor();

console.log(p2.constructor === Person); // false
console.log(p2 instanceof Person); // false
console.log(p2 instanceof Person.constructor); // true

类是 JavaScript 中的一等公民,这意味着它们可以像传递其他对象或函数引用一样传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 类可以在函数能定义的任何地方定义,比如在数组中:
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); // 实例 3141

与直接调用的函数表达式相似,类也可以直接实例化:

1
2
3
4
5
6
7
// 因为是一个类表达式,所以类名是可选的
let p = new (class Foo {
constructor(x) {
console.log(x);
}
})('bar'); // bar
console.log(p); // Foo {}

实例成员、原型成员和类自身成员

类定义的语法允许简洁地定义对象实例上的成员、对象原型上的成员以及类本身的成员。

实例成员

每次调用new <classname> 时,构造函数都将执行。在这个函数内部,可以用任何属性填充新创建的实例(this 对象)。对于可以添加到新实例的内容没有限制,对于在构造函数退出后添加的成员也没有限制。

每个实例都分配了唯一的成员对象,也就是说原型上没有任何东西是共享的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
constructor() {
// 对于这个例子来说,使用对象封装器定义一个字符串,来检查不同实例间是否共享
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
p1.name = p1.nicknames[0];
p2.name = p2.nicknames[1];
p1.sayName(); // Jake
p2.sayName(); // J-Dog
原型方法和访问器

为了允许不同实例之间共享方法,类的定义语法允许在类体内的原型对象上定义方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
constructor() {
// 任何定义在this的内容将会是实例自己单独拥有
this.locate = () => console.log('instance');
}
// 任何直接定义在类定义语句块内的内容将会在类原型对象上拥有
locate() {
console.log('prototype');
}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype

方法可以在任何一个位置定义,但成员数据,如原始类型和对象,不能添加到类定义语句块内的原型上:

1
2
3
4
class Person {
name: 'Jake';
}
// Uncaught SyntaxError: Unexpected token :

类方法的行为与对象属性相同,这意味着它们可以用字符串、symbols 或计算值作为键值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const symbolKey = Symbol('symbolKey');
class Person {
stringKey() {
console.log('invoked stringKey');
}
[symbolKey]() {
console.log('invoked symbolKey');
}
['computed' + 'Key']() {
console.log('invoked computedKey');
}
}
let p = new Person();
p.stringKey(); // invoked stringKey
p[symbolKey](); // invoked symbolKey
p.computedKey(); // invoked computedKey

类定义也支持 getter 和 setter 访问器。语法和行为与普通对象相同:

1
2
3
4
5
6
7
8
9
10
11
class Person {
set name(newName) {
this.name_ = newName;
}
get name() {
return this.name_;
}
}
let p = new Person();
p.name = 'Jake';
console.log(p.name); // Jake
类静态方法和访问器

也可以在类自身上定义方法。在函数执行时不以特定实例为中心并且实际上不需要实例存在的情况下,可以使用这些类。像原型成员一样,每个类静态成员只需创建一次。

在类定义中,使用 static 关键字作为前缀来指定静态类成员。在静态成员内部,this 指的是类本身。所有的其他约定与原型成员相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
constructor() {
// 任何定义在this的内容将会是实例自己单独拥有
this.locate = () => console.log('instance', this);
}
// 在类的原型对象上定义
locate() {
console.log('prototype', this);
}
// 在类上定义
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, {constructor: ... }
Person.locate(); // class, class Person {}

这些静态类方法经常作为实例的工厂函数使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
constructor(age) {
this.age_ = age;
}
sayAge() {
console.log(this.age_);
}
static create() {
// 创建并返回一个随机年龄的person实例
return new Person(Math.floor(Math.random() * 100));
}
}
console.log(Person.create()); // Person { age_:... }
非函数的原型成员和类成员

尽管类定义并不显式支持向原型或类添加数据成员,但在类定义之外可以手动添加它们:

1
2
3
4
5
6
7
8
9
10
11
class Person {
sayName() {
console.log('${Person.greeting} ${this.name}');
}
}
// 在类上定义数据
Person.greeting = 'My name is';
// 在原型上定义数据
Person.prototype.name = 'Jake';
let p = new Person();
p.sayName(); // My name is Jake

注意:不显式允许定义属性的主要原因是,共享对象中的可变数据成员可能是反面模式。通常,对象实例应该直接拥有它们从 this 引用的数据。

迭代器和生成器方法

类定义的语法允许在原型和类本身上定义生成器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person {
// 在原型上定义生成器
*createNicknameIterator() {
yield 'Jack';
yield 'Jake';
yield 'J-Dog';
}
// 在类上定义生成器
static *createJobIterator() {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker';
}
}

let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); // Candlestick maker

let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog

因为支持生成器方法,所以可以通过添加默认迭代器使类实例可迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}
*[Symbol.iterator]() {
yield* this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// Jack
// Jake
// J-Dog

或者只返回一个迭代器实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}
[Symbol.iterator]() {
return this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// Jack
// Jake
// J-Dog

继承

在本章的前文中,讨论了使用 ES5 机制实现继承的繁琐细节。ECMAScript6 规范中最好的补充之一是对类的继承机制的原生支持。尽管使用了一种新的语法,但类的继承本质上仍然使用原型链。

继承基础

ES6 类支持单一继承格式。使用 extends 关键字,可以继承任何具有[[Construct]]属性和原型的对象。在大多数情况下是从另一个类继承,但这也允许继承自构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
class Vehicle {}
// 继承自类
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
function Person() {}
// 继承自构造函数
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true

类静态方法和原型方法都传递到子类。this 值反映了调用方法的类和实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Vehicle {
identifyPrototype(id) {
console.log(id, this);
}
static identifyClass(id) {
console.log(id, this);
}
}
class Bus extends Vehicle {}
let v = new Vehicle();
let b = new Bus();
b.identifyPrototype('bus'); // bus, Bus {}
v.identifyPrototype('vehicle'); // vehicle, Vehicle {}
Bus.identifyClass('bus'); // bus, class Bus {}
Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {}

注意:extends 关键字在类表达式中是有效的,所以 let Bar = class extends Foo {}是完全有效的语法。

Constructors、HomeObjects 和 super()

子类的方法可以通过 super 关键字去引用它们的原型。这只对子类有效,并且只能在 constructor 或静态方法内部使用。在 constructor 内部使用 super 来控制何时调用父类的 constructor。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Vehicle {
constructor() {
this.hasEngine = true;
}
}
class Bus extends Vehicle {
constructor() {
// 在super之前不能引用this,否则会抛出ReferenceError
super(); // 与super.constructor()相同
console.log(this instanceof Vehicle); // true
console.log(this); // Bus { hasEngine: true }
}
}
new Bus();

也可以在静态方法内部使用 super 来调用父类上定义的静态方法:

1
2
3
4
5
6
7
8
9
10
11
class Vehicle {
static identify() {
console.log('vehicle');
}
}
class Bus extends Vehicle {
static identify() {
super.identify();
}
}
Bus.identify(); // vehicle

注意:ES6 给构造函数和静态方法一个内部[[HomeObject]]的引用,指向定义方法的对象。这个指针是自动分配的,并且只能在 JavaScript 引擎内部访问。Super 总是被定义为[[HomeObject]的原型。

使用 super 时需要注意的事项:

➤ 仅能在子类构造函数或静态方法中使用。

1
2
3
4
5
6
class Vehicle {
constructor() {
super();
// SyntaxError: 'super' keyword unexpected
}
}

➤ super 关键字本身不能引用;它必须作为构造函数调用,或者用于静态方法中引用。

1
2
3
4
5
6
7
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(super);
// SyntaxError: 'super' keyword unexpected here
}
}

➤ 调用 super()时将调用父类 constructor 且会将生成的实例赋给 this。

1
2
3
4
5
6
7
8
class Vehicle {}
class Bus extends Vehicle {
constructor() {
super();
console.log(this instanceof Vehicle);
}
}
new Bus(); // true

➤ super()表现得像一个构造函数,必须手动的传参给 super()以传给父类的 constructor。

1
2
3
4
5
6
7
8
9
10
11
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {
constructor(licensePlate) {
super(licensePlate);
}
}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }

➤ 如果子类不定义 constructor,那么将自动调用 super(),并将所有参数传递给父类的 constructor。

1
2
3
4
5
6
7
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }

➤ 在调用 super()之前,不能在 constructor 中引用 this。

1
2
3
4
5
6
7
8
9
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(this);
}
}
new Bus();
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor

➤ 如果类派生自父类,并且显式定义了 constructor,则必须调用 super()或从返回一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor() {
super();
}
}
class Van extends Vehicle {
constructor() {
return {};
}
}
console.log(new Car()); // Car {}
console.log(new Bus()); // Bus {}
console.log(new Van()); // {}
抽象基类

有时可能会需要定义一个应该从中继承但不能直接实例化的抽象基类。尽管 ECMAScript 不显式支持此功能,但使用 new.target 可以轻松实现,它会告诉你与 new 关键字一起使用的内容。可以通过检查 new.target 是否为抽象基类来防止直接实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 抽象基类
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

也可能需要通过在抽象基类 constructor 中检查子类上定义的方法。因为原型方法在调用 constructor 之前就已经存在,所以可以在 this 关键字上检查它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 抽象基类
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(); // 成功!
new Van(); // Error: Inheriting class must define foo()
内置类型继承

ES6 类与现有内置引用类型可以无缝互操作,因此可以轻松地扩展它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SuperArray extends Array {
shuffle() {
// Fisher-Yates洗牌算法
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this[i], this[j]] = [this[j], this[i]];
}
}
}
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); // [3, 1, 4, 5, 2]

一些内置类型定义了返回新实例的方法。默认情况下,该实例可以匹配子类的类型:

1
2
3
4
5
6
7
class SuperArray extends 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); // true
console.log(a2 instanceof SuperArray); // true

如果要重写此方法,可以重写 Symbol.species 访问器,该访问器用于确定用于返回实例的类:

1
2
3
4
5
6
7
8
9
10
11
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); // true
console.log(a2 instanceof SuperArray); // false
多继承

Javascript 中的一个常见模式是将来自几个不同类的行为捆绑到一个包中。尽管 ES6 类不显式支持多继承,但提供了可以巧妙运用的扩展方式以模拟这种行为。

注意:Object.assign()方法旨在从多个对象中提供混合行为。只有当采用类的形式时,才需要实现自己的 mixin 表达式。如果只需要在多个对象之间合并属性,则应该使用 Object.assign()。

Extends 关键字后面的引用是一个 JavaScript 表达式。只要解析为类或函数构造函数,任何语法在那个位置都是有效的。当类定义被计算时时,表达式也会被求值:

1
2
3
4
5
6
7
class Vehicle {}
function getParentClass() {
console.log('evaluated expression');
return Vehicle;
}
class Bus extends getParentClass() {}
// evaluated expression

可以通过在表达式中链接多混入元素来实现多继承模式,这些元素将解析为一个可以从中继承的类。如果一个类 Person 需要混入 A、B 和 C,那么可以构建一个模式,将 B 从 A 继承,C 从 B 继承,Person 从 C 继承,从而将所有三个类连接到超类中。有几种策略可以实现这种模式。

一种策略是定义接受超类作为参数的嵌套函数,将混入类定义为参数的子类,并返回该类。这些混入类可以在彼此内部链接,并作为超类表达式提供:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Vehicle {}
let FooMixin = (Superclass) =>
class extends Superclass {
foo() {
console.log('foo');
}
};
let BarMixin = (Superclass) =>
class extends Superclass {
bar() {
console.log('bar');
}
};
let BazMixin = (Superclass) =>
class extends Superclass {
baz() {
console.log('baz');
}
};
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz

也可以使用一个工具函数使嵌套扁平化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Vehicle {}
let FooMixin = (Superclass) =>
class extends Superclass {
foo() {
console.log('foo');
}
};
let BarMixin = (Superclass) =>
class extends Superclass {
bar() {
console.log('bar');
}
};
let BazMixin = (Superclass) =>
class extends Superclass {
baz() {
console.log('baz');
}
};
function mix(BaseClass, ...Mixins) {
return Mixins.reduce(
(accumulator, current) => current(accumulator),
BaseClass
);
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz

注意:许多 JavaScript 框架,特别是 React,正在从混入(mixin)模式转向组合(composition)模式(提取方法到单独的类和工具,并在不使用继承的情况下合并这些零碎的方法)。这反映了众所周知的“组合优于继承”的软件设计原则,许多人认为这一原则提供了卓越的灵活性和代码设计。

👆 全文结束,棒槌时间到 👇