什么? chatgpt 居然告诉我 JS 中常见的继承方案有十种?

1,047 阅读21分钟

引言

继承 是面向对象编程中讨论最多的话题, 在 JS 中 继承 主要是通过 原型、原型链 实现的, 为了了解更多关于 JS 继承 的细节, 我问了 chatgpt 如下问题:

image.png

JS 中常用的 继承 方案到底有哪些? 对于 chatgpt 给出的答案我是表示怀疑的, 所以我翻出珍藏多年的 《JavaScript 高级程序设计 (第4版)》 进行查验, 并总结如下:

一、原型链继承

1.1 基本思想

通过将 子类 的 原型对象 指向 父类 的 实例对象 来实现 继承, 如下代码:

  • 定义了两个 类型 数据分别是 Parent 和 Child
  • Parent 类型定义了 name 和 age 属性并且在 原型 上声明了方法 getName() 用于输出实例的 name 属性值
  • Child 类型则定义了 name 属性, 并且将 原型 指向了 Parent 类型的一个 实例对象, 同时还往 原型对象 上新增了方法 getAge() 用于输出实例的 age 属性值
function Parent() {
  this.name = 'parent';
  this.age = '18';
}
Parent.prototype.getName = function () {
  console.log('name:', this.name);
};


function Child() {
  this.name = 'child';
}
Child.prototype = new Parent(); // 子类的原型指向父类的实例
Child.prototype.getAge = function () {
  console.log('age:', this.age);
};

var child = new Child();

child.getName(); // name: child
child.getAge();  // age: 18
child.name // child
child.age // 18

如上代码, 通过将子类 child 的 原型 指向父类 Parent 的 实例对象, 使得 子类实例对象 能够访问到 父类实例对象 的属性、以及父类 Parent 原型上定义的方法, 下图展示了相关实例、构造函数、原型之间的关系, 绿色箭头是 Child 实例的一个 原型链

image.png

1.2 原型终点

上文我们简单绘制了下 Child 实例的一个 原型链, 但实际上我们并没有绘制完整, 正如 《原型、原型链》 文中提到的, 所有 原型链 的终点都将是 Object.prototype -> null, 下图是补全后的 原型链:

image.png

关系图验证: 将上面代码复制到浏览器控制台, 输出 child 来查看实例

image.png

1.3 实例和原型的关系

在 JS 中我们有 两种 方式可以来判断 原型 是否存在于某个 实例 的 原型链 上

  1. 使用 instanceof 运算符, 可检测 构造函数 的 prototype 属性是否出现在 实例 对象的 原型链 上,
child instanceof Child // Child.prototype 是否在 child 原型链上 => true
child instanceof Parent // Parent.prototype 是否在 child 原型链上 => true
child instanceof Object // Object.prototype 是否在 child 原型链上 => true
  1. 使用 isPrototypeOf() 方法, 可检测 一个对象 是否存在于 另一个对象 的 原型链 上
Child.prototype.isPrototypeOf(child) // Child.prototype 是否在 child 原型链上 => true
Parent.prototype.isPrototypeOf(child) // Parent.prototype 是否在 child 原型链上 => true
Object.prototype.isPrototypeOf(child) // Object.prototype 是否在 child 原型链上 => true

补充说明: isPrototypeOf() 是 Object.prototype 上的一个方法

1.4 二个缺点

  1. 原型 中包含 引用值 的问题: 如果 原型 中包含了 引用值, 那么这个值会在 所有实例 间进行共享, 如下代码 Child.prototype 中包含了引用值 address, 该值会在所有 Child 实例对象中进行共享, 在 child1 中我们往 address 新增了一个值, child2 中的 address 也会被改变
function Parent() {
  this.address = ['北京', '上海']
}

function Child() {}
Child.prototype = new Parent(); // 子类的原型指向父类的实例

const child1 = new Child();
child1.address.push('杭州') 
console.log(child1.address) // [ '北京', '上海', '杭州' ]

const child2 = new Child();
console.log(child2.address) // [ '北京', '上海', '杭州' ]

上面代码对应实例、构造函数、原型图如下, 其中绿色表示实例的 原型链

image.png

关系图验证: 将上面代码复制到浏览器控制台, 输出 child1 child2 来查看实例

image.png

补充: 这也是为什么 属性 通常会在 构造函数 中定义, 而不会直接在 原型 上进行定义的原因

  1. 子类 在 实例化 时不能给 父类 的构造函数传参: 如下代码, Parent 构造函数是支持传递参数的, 但是呢, 我们无法在执行 Child 构造函数时, 为 Parent 传递不同参数, 只能在为 Child 绑定 原型 时给定一个固定的参数
function Parent(name) {
  this.name = name
  this.address = ['北京', '上海']
}

function Child() {}
Child.prototype = new Parent('moyuanjun'); // 子类的原型指向父类的实例

const child1 = new Child('想要把这参数传给 Parent 类, 传不了');
child1.name // moyuanjun

补充: 原型链继承 缺点相对比较明显, 所以基本不会被单独使用

1.5 注意事项

  1. 为 原型 添加 方法 或 属性, 应该在 原型 设置之后再进行: 如下代码在设置 原型 前添加的 属性 或 方法 会丢失, 因为添加到 初始原型对象 上了, 后面修改了 原型 就导致这些 属性、方法 都丢失了
function Parent() {
  this.address = ['北京', '上海']
}

function Child() {}
// 在重新设置原型前添加了 原型方法 getAddress
Child.prototype.getAddress = () => {
  console.log('address:', this.address)
}

// 修改 prototype 指向, 之前设置的所有原型方法、属性都将丢失
Child.prototype = new Parent(); // 子类的原型指向父类的实例

const child = new Child();

console.log(child.address) // [ '北京', '上海' ]
child.getAddress() // TypeError: child.getAddress is not a function
  1. 避免通过 字面量形式 修改 prototype(原型): 如下代码, 设置完 原型 后, 又通过字面量形式修改了 prototype 导致前面设置的 原型 失效
function Parent() {
  this.address = ['北京', '上海']
}

function Child() {}
Child.prototype = new Parent(); // 子类的原型指向父类的实例

// 以字面量形式修改了原型, 导致 prototype 指向改变了
Child.prototype = {
  getAddress: () => {}
}
const child = new Child();

二、盗用构造函数

2.1 基本思想

通过在 子类构造函数 中, 调用 父类构造函数 来实现继承, 如下代码: 在子类 Child 构造函数中调用父类 Parent 构造函数, 并通过 call 来指定 this 对象, 这样执行 Parent 构造函数时创建的属性、方法就都会挂载在 Child 实例上

function Parent(name) {
  this.name = name
  this.address = ['北京', '上海', '杭州']
}

function Child({ name }) {
  Parent.call(this, name)
}

const child = new Child({ name: 'moyuanjun' });

child.address // ['北京', '上海', '杭州']
child.name // moyuanjun

上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

image.png

关系图验证: 将上面代码复制到浏览器控制台, 输出 child 来查看实例

image.png

2.2 两个优点

  1. 可解决上文提到的引用值问题: 每次执行 Child 构造函数时, 将调用 Parent 的 构造函数, 往当前实例对象 新增 属性、方法, 这些属性、方法都是 独立 的和其他实例 隔离 开来
function Parent(name) {
  this.name = name
  this.address = ['北京', '上海']
}

function Child({ name }) {
  Parent.call(this, name)
}

const child1 = new Child({ name: 'moyuanjun' });
child1.address.push('杭州')

const child2 = new Child({ name: 'jiaolian' });
child1.address // ['北京', '上海', '杭州']
child2.address // ['北京', '上海']

上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

image.png

  1. 支持为父类构造函数传参: 如下代码, 在执行子类构造函数 Child 时可以为父类 Parent 进行传参
function Parent(name) {
  this.name = name
  this.address = ['北京', '上海']
}

function Child({ name }) {
  // 未父类透传参数
  Parent.call(this, name)
}

const child1 = new Child({ name: 'moyuanjun' });
const child2 = new Child({ name: 'jiaolian' });
child1.name // moyuanjun
child2.name // jiaolian

2.3 两个缺点

  1. 不能共用属性、方法: 严格意义上可能并不算是继承, 有点像是构造函数 逻辑 的抽离, 每次在实例化时都 新建 了属性、方法, 导致属性和方法都无法被共用, 但是实际上 方法 是应该允许被共用的, 才比较合理
function Parent(name) {
  this.name = name
  this.address = ['北京', '上海']
  this.getAddress = () => this.address
}

function Child({ name }) {
  Parent.call(this, name)
}

const child1 = new Child({ name: 'moyuanjun' });
const child2 = new Child({ name: 'jiaolian' });

child1.getAddress === child2.getAddress // false, 不是同一个方法
  1. instanceof 操作符和 isPrototypeOf() 方法无法识别出 合成对象 继承于哪个父类, 因为并没有针对原型、原型链进行修改
child1 instanceof Child // true
child1 instanceof Parent // false
child1 instanceof Object // true

Child.prototype.isPrototypeOf(child1) // true
Parent.prototype.isPrototypeOf(child1) // false
Object.prototype.isPrototypeOf(child1) // true

补充: 盗用构造函数继承 缺点相对比较明显, 所以基本不会被单独使用

三、组合继承

3.1 基本思想

综合了 原型链继承 和 盗用构造函数继承, 将两者的优点集中了起来, 使用 原型链继承 继承了原型上的属性和方法, 并通过 盗用构造函数 继承了 实例属性, 这样既可以把方法定义在原型上以实现共用, 又可以让每个实例都有自己的属性

function Parent(name) {
  this.name = name
  this.address = ['北京', '上海']
}
Parent.prototype.sayName = function(){
  console.log('name:', this.name)
}

function Child({ name, age }) {
  Parent.call(this, name) // 继承属性
  this.age = age
}
Child.prototype = new Parent(); // 继承方法
Child.prototype.sayAge = function(){
  console.log('age:', this.age)
}

const child1 = new Child({ name: 'moyuanjun', age: 20 });
child1.address.push('杭州')
child1.address // [ '北京', '上海', '杭州' ]
child1.sayName() // name: moyuanjun
child1.sayAge() // age: 20

const child2 = new Child({ name: 'jiaolian', age: 18 });
child2.address // ['北京', '上海']
child2.sayName() // name: jiaolian
child2.sayAge() // age: 18

child1.sayName === child2.sayName // true
child1.sayAge === child2.sayAge // true

上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

image.png

关系图验证: 将上面代码复制到浏览器控制台, 并输出 child1 和 child2 得到如下内容:

image.png

3.2 优点

  • 组合继承 弥补了 原型链继承 和 盗用构造函数继承 的不足
  • 组合继承 也保留了 instanceof 操作符和 isPrototypeOf() 方法识别 合成对象 的能力
child1 instanceof Child // true
child1 instanceof Parent // true
child1 instanceof Object // true

Child.prototype.isPrototypeOf(child1) // true
Parent.prototype.isPrototypeOf(child1) // true
Object.prototype.isPrototypeOf(child1) // true

补充: 组合继承 是 JS 中使用 最多 的 继承模式

3.3 缺点

存在效率问题, 在为 子类 设置 原型 时会额外调用一次 父类构造函数, 会创建 无用 的属性和方法, 这些属性方法在执行 子类构造函数 时还会被创建一次, 所以子类原型里的这些属性、方法是会 被屏蔽 的, 如下代码: 在设置 Child.prototype 时会调用一次 Parent, 之后每次执行 Child 构造函数都会再次被调用

function Parent(name) {
  this.name = name
  this.address = ['北京', '上海']
  this.sayName = () => {}
}

function Child({ name, age }) {
  // 调用: Parent
  Parent.call(this, name) 
  this.age = age
}

// 调用 Parent, 会创建无效属性、方法: name address sayName
Child.prototype = new Parent(); // 继承方法
const child = new Child({ name: 'moyuanjun', age: 20 });

上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

image.png

关系图验证: 将上面代码复制到浏览器控制台, 并输出 child 得到如下内容:

image.png

四、原型式继承

4.1 基本思想

通过一个 函数, 函数内部会创建一个 临时构造函数, 并将 传入的对象 作为这个构造函数的 原型, 最后返回这个临时类型的一个 实例 来实现继承, 其实该继承和原型链继承很相似, 只是 原型式继承 不需要自定义类型, 可以快速基于某个对象创建新的对象

function object(o) { 
  function F() {} // 临时构造函数
  F.prototype = o; // 临时构造函数.原型 = 传入的对象
  return new F(); // 返回临时构造函数对应实例对象
}

const parent = {
  name: 'moyuanjun',
  age: 18,
  sayName: function() {
    console.log('name:', this.name)
  }
}

const child = object(parent)

child.name // moyuanjun
child.age // 18
child.sayName() // name: moyuanjun

上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

image.png

关系图验证: 将上面代码复制到浏览器控制台, 输出 child 来查看实例

image.png

补充: 原型式继承 非常适合 不需要 单独创建构造函数, 但仍然需要 在对象间 共享信息的场合, 比较适用的一个场景是, 基于现有的一个对象创建新的对象, 并进行适当的修改

4.2 Object.create()

ES6 通过增加 Object.create() 方法, 将 原型式继承 的概念规范化了, 我们可以借用 Object.create() 实现 原型式继承, 可基于某个对象创建一个新的对象

const parent = {
  name: 'moyuanjun',
  age: 18,
  sayName: function() {
    console.log('name:', this.name)
  }
}

const child = Object.create(parent)

child.name // moyuanjun
child.age // 18
child.sayName() // name: moyuanjun

4.3 缺点

原型 中包含 引用值 的问题: 跟使用 原型模式 类似, 在 原型式继承 中, 引用值属性 始终会在相关对象间 共享

const parent = {
  address: ['北京', '上海'],
}

const child1 = Object.create(parent)
child1.address.push('杭州')

const child2 = Object.create(parent)

child2.address // [ '北京', '上海', '杭州' ]
child1.address === child2.address // true

上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

image.png

关系图验证: 将上面代码复制到浏览器控制台, 输出 child1 child2 来查看实例对象

image.png

五、寄生式继承

5.1 基本思想

寄生式继承 与 原型式继承 很接近, 背后的思路类似于 寄生构造函数模式 和 工厂模式, 基本思路就是创建一个实现继承的 函数, 以 某种方式 增强对象, 然后返回这个对象, 如下代码所示:

function createAnother (original) {
  // 调用某个函数, 返回新对象
  const obj = Object.create(original) 

  // 以某种方式增强这个对象
  obj.sayHi = function(){
    console.log('hi')
  }

  // 返回这个对象
  return obj
}

const parent = {
  age: 18,
  name: 'moyuanjun',
}

const child = createAnother(parent)

child.sayHi() // hi
child.name // moyuanjun
child.age // 18

上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

image.png

关系图验证: 将上面代码复制到浏览器控制台, 输出 child 来查看实例对象

image.png

补充: 寄生式继承 同样适合主要关注对象, 而不在乎 类型 和 构造函数 的场景, 同时需要注意的是 Object.create() 函数不是 寄生式继承 所 必需 的, 任何 返回新对象 的函数都可以在这里使用

5.2 缺点

通过 寄生式继承 给对象添加的 函数 是难以被 复用 的, 如下代码, 每次通过 createAnother 创建实例都会重新声明、挂载 sayHi() 函数, 每个实例的 sayHi() 都是独立的无法复用

function createAnother (original) {
  // 通过调用函数创建一个新对象
  const obj = Object.create(original) 

  // 以某种方式增强这个对象
  obj.sayHi = function(){
    console.log('hi')
  }

  // 返回这个对象
  return obj
}

const parent = {
  age: 18,
  name: 'moyuanjun',
}

const child1 = createAnother(parent)
const child2 = createAnother(parent)

child1.sayHi === child2.sayHi // false

上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

image.png

六、寄生式组合继承

在 组合继承 中我们提到, 该继承方案是存在效率问题的, 在设置子类原型时会调用一次 父类构造函数 会创建一些无用的属性、方法, 然而本质上, 子类原型最终只要包含 超类(父类) 对象的所有实例属性即可, 同时 子类 构造函数只要在执行时重写自己的原型就行了

6.1 基本思路

在 组合继承 的思想基础之上进行优化, 修改 子类原型 时不再 直接创建 父类的实例, 而是通过 寄生式继承 来 继承父类原型, 然后将返回的新对象 作为 子类原型, 如下代码: 对上文中 组合继承 代码进行了优化

+ function inheritPrototype(child, parent) { 
+   // 1. 创建父类原型的一个副本
+   const prototype = Object.create(parent.prototype); 
+ 
+   // 2. 给返回的 prototype 对象设置 constructor 属性, 解决由于重写原型导致默认 constructor 丢失问题
+   prototype.constructor = child; 
+ 
+   // 3. 将新创建的对象赋值给子类型的原型
+   child.prototype = prototype; 
+ }

function Parent(name) {
  this.name = name
  this.address = ['北京', '上海']
}
Parent.prototype.sayName = function(){
  console.log('name:', this.name)
}

function Child({ name, age }) {
  Parent.call(this, name) // 继承属性
  this.age = age
}

+ // 使用「寄生式继承」来继承父类原型
+ // 组合继承这里是通过 Child.prototype = new Parent(); 来实现继承的
+ inheritPrototype(Child, Parent);

Child.prototype.sayAge = function(){
  console.log('age:', this.age)
}

const child1 = new Child({ name: 'moyuanjun', age: 20 });
child1.address.push('杭州')
child1.address // [ '北京', '上海', '杭州' ]
child1.sayName() // name: moyuanjun
child1.sayAge() // age: 20

const child2 = new Child({ name: 'jiaolian', age: 18 });
child2.address // ['北京', '上海']
child2.sayName() // name: jiaolian
child2.sayAge() // age: 18

child1.sayName === child2.sayName // true
child1.sayAge === child2.sayAge // true

上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

image.png

关系图验证: 将上面代码复制到浏览器控制台, 输出 child1 来查看实例对象

image.png

6.2 优点

使用 寄生式继承 来弥补 组合继承 的缺点, 设置子类原型时只会对父类原型进行拷贝, 而不是创建父类实例对象, 这样可以避免创建无用是属性、方法, 寄生式组合继承 可以算是 引用类型 继承的最佳模式

七、Class 继承

上文提到的各种继承策略都有自己的问题, 也有相应的妥协; 正因为如此, 实现继承的代码也显得非常 冗长 和 混乱; 为解决这些问题 ES6 新引入的 class 关键字具有正式定义类的能力, 类(class) 是 ES6 中新的基础性语法糖结构, 虽然 class 从表面上看起来可以支持正式的面向对象编程, 但实际上它背后使用的 仍然 是 原型、原型链 和 构造函数 的概念

7.1 实现继承

使用 extends 关键字, 就可以继承任何拥有 [[Construct]] 和 原型 的对象, 很大程度上, 这意味着不仅可以 继承 一个 类, 也可以 继承 普通的 构造函数 (向后兼容)

  1. 继承 Class 类
class Parent {
  sayHi(){
    console.log('hi')
  }
}

class Child extends Parent {}

const child = new Child(); 
child.sayHi() // hi
console.log(child instanceof Child);    // true
console.log(child instanceof Parent); // true
  1. 继承普通构造函数
function Person() {
  this.sayHi = function(){
    console.log('hi')
  }
}
class Engineer extends Person {
  name = 'jiaolian'
}
const engineer = new Engineer(); 
engineer.sayHi() // hi
console.log(engineer instanceof Engineer);  // true
console.log(engineer instanceof Person);   // true

7.2 调用父类构造函数

在类构造函数中使用 super() 可以调用父类构造函数

class Parent {
  constructor(name){
    this.name = name
  }
}

class Child extends Parent {
  constructor(name, age){
    super(name) // 调用父类构造函数, 并进行传参
    this.age = age
  }
}

const child = new Child('moyuanjun', 18)

child.name // moyuanjun
child.age // 18

7.3 抽象基类

有时候可能需要定义这样一个类, 它可供其他类继承, 但本身是不允许被实例化, 虽然 ES6 没有专门支持这种类的语法, 但我们可以通过 new.target 来实现, 在实例化过程中我们可以通过 new.target 获取到当前正在实例化的类或构造函数, 通过判断 new.target 就可以阻止对抽象基类的实例化

// 抽象基类 
class Vehicle {
  constructor() { 
    // 通过 new.target 判断, Vehicle 是否正在被被实例化
    if (new.target === Vehicle) { 
      throw new Error('Vehicle cannot be directly instantiated');
    }
  }
}

class Bus extends Vehicle {}

new Bus(); 
new Vehicle(); // Error: Vehicle cannot be directly instantiated

7.4 继承内置类型

ES6 类为继承 内置引用类型 提供了顺畅的机制, 开发者可以方便地扩展内置类型

// 继承内置类型 Array 进行扩展
class SuperArray extends Array {
  // 洗牌算法
  shuffle() { 
    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]];
    }
  }  
}

const arr = new SuperArray(1, 2, 3, 4, 5);

console.log(arr instanceof Array);      // true
console.log(arr instanceof SuperArray); // true

console.log(arr);  // [1, 2, 3, 4, 5]
arr.shuffle();
console.log(arr); // [3, 1, 4, 5, 2]

有些内置类型的 方法 会返回 新实例, 默认情况下, 这些方法返回 实例的类型 与 原始实例 的类型是一致的

class SuperArray extends Array {}

const arr1 = new SuperArray(1, 2, 3, 4, 5)
// map 返回类型和 arr1 的类型是一致的, 为 SuperArray
const arr2 = arr1.map(v => v) 

console.log(arr1 instanceof SuperArray); // true
console.log(arr2 instanceof SuperArray); // true

如果想覆盖这个默认行为, 可通过覆盖 Symbol.species 访问器来实现, 这个访问器决定 方法 在返回 新实例 时所使用的类

class SuperArray extends Array {
+ // 覆盖 Symbol.species 访问器, 当创建返回的新实例时实例的类型
+ static get [Symbol.species]() {
+   return Array
+ }
}

const arr1 = new SuperArray(1, 2, 3, 4, 5)
// map 返回类型和 arr1 的类型是一致的, 为 SuperArray
const arr2 = arr1.map(v => v) 

console.log(arr1 instanceof SuperArray); // true
+ console.log(arr2 instanceof SuperArray); // false

7.5 类混入

把不同类的行为集中到一个类, 是一种常见的 JS 模式, 虽然 ES6 没有显式支持多类继承, 但通过现有特性可以轻松地模拟这种行为

  1. 前置知识: extends 关键字后面可以是一个 JS 表达式, 表达式 的值只要是一个类、或者构造函数即可
const num = 2

class Base1 {
  age = 18
  name = 'moyuanjun'
}

class Base2 {
  age = 20
  name = 'jiaolian'
}

function getParentClass() { 
  return Base1; 
}

class Child1 extends getParentClass() {}
class Child2 extends (num === 1 ? Base1 : Base2) {}

const child1 = new Child1() // Child1 { age: 18, name: 'moyuanjun' }
const child2 = new Child2() // Child2 { age: 20, name: 'jiaolian' }
  1. 在实际开发中如果只是需要混入多个对象的属性, 则只需要使用 Object.assign(), 该方法就是为了混入对象行为而设计的
const obj = Object.assign({ name: 'moyuanjun' }, { age: 18 })
obj // { name: 'moyuanjun', age: 18 }  
  1. 混入的方式有很多策略, 常见的策略是定义一组 可嵌套 函数, 每个函数分别接收一个 超类(父类) 作为参数, 而将 混入类 定义为这个参数的子类, 并返回这个类, 这些组合函数可以连缀调用, 最终组合成 超类(父类) 表达式, 如下代码所示:
  • Foo、Bar、Baz 是一组函数, 接收一个 超类(父类), 函数内部继承于 超类(父类) 进行扩展, 返回一个新的子类
  • Foo、Bar、Baz 函数进行嵌套执行, 返回一个混合类 Child, 最后再基于这个 混合类 进行扩展
class Base {}

const Foo = (Superclass) => class Foo extends Superclass { 
  foo() {
    console.log('foo'); 
  } 
}; 

const Bar = (Superclass) => class Bar extends Superclass { 
  bar() { 
    console.log('bar'); 
  } 
}; 
  
const Baz = (Superclass) => class Baz extends Superclass { 
  baz() { 
    console.log('baz'); 
  }
};

class Child extends Baz(Bar(Foo(Base))) {}

let child = new Child(); 
child.foo(); // foo 
child.bar(); // bar 
child.baz(); // baz
  1. 这里可以通过写一个辅助函数, 将嵌套调用展开, 如下代码新增了 mix 函数, 使用 reduce 将所有传入的函数嵌套展开
+ function mix(BaseClass, ...Mixins) { 
+   return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass); 
+ }

class Base {}

const Foo = (Superclass) => class Foo extends Superclass { 
  foo() {
    console.log('foo'); 
  } 
}; 

const Bar = (Superclass) => class Bar extends Superclass { 
  bar() { 
    console.log('bar'); 
  } 
}; 
  
const Baz = (Superclass) => class Baz extends Superclass { 
  baz() { 
    console.log('baz'); 
  }
};

+ class Child extends mix(Base, Foo, Bar, Baz) {}

let child = new Child(); 
child.foo(); // foo 
child.bar(); // bar 
child.baz(); // baz

补充: 很多 JS 框架 (特别是 React) 已经抛弃混入模式, 转向了组合模式, 该模式的思想就是将方法提取到独立的类和辅助对象中, 然后把它们组合起来, 而不是使用继承, 这反映了那个众所周知的软件设计原则 组合胜过继承(composition over inheritance) 这个设计原则被很多人遵循, 在代码设计中能提供极大的灵活性

八、总结

8.1 原型链继承

  • 思路: 通过将 子类 的 原型对象 指向 父类 的 实例对象 来实现 继承
  • 缺点: 原型 如果包含 引用值, 修改 引用值 所有 实例 都会改动到
  • 缺点: 子类 在 实例化 时不能给 父类 的 构造函数 传参

8.2 盗用构造函数

  • 思路: 在 子类构造函数 中, 调用 父类构造函数 来实现继承
  • 优点: 可解决上文提到的 引用值 问题, 每个 实例 都是新建一个 引用值
  • 优点: 支持为父类构造函数传参
  • 缺点: 不能共用属性、方法, 每次都是重新创建
  • 缺点: instanceof 操作符和 isPrototypeOf() 方法无法识别出 合成对象 继承于哪个父类

8.3 组合继承

  • 思路: 使用 原型链 继承原型上的属性和方法, 通过 盗用构造函数 继承父类属性
  • 优点: 组合继承 弥补了 原型链继承 和 盗用构造函数继承 的不足
  • 优点: 组合继承 也保留了 instanceof 操作符和 isPrototypeOf() 方法识别 合成对象 的能力
  • 缺点: 存在效率问题, 在为 子类 设置 原型 时会额外调用一次 父类构造函数, 会创建无效的属性、方法

8.4 原型式继承

  • 思路: 通过一个 函数, 函数内部会创建一个 临时构造函数, 并将 传入的对象 作为这个构造函数的 原型, 最后返回这个临时类型的一个 实例 来实现继承
  • 适用场景: 基于现有的一个对象, 的基础之上创建新的对象, 并进行适当的修改
  • 缺点: 跟使用 原型模式 类似, 在 原型式继承 中, 引用值属性 始终会在相关对象间 共享
  • 补充: ES5 通过增加 Object.create() 方法将 原型式继承 的概念规范化, 和 原型链继承 的区别在于 原型式继承 不需要自定义类型, 直接通过一个函数来实现继承

8.5 寄生式继承

  • 思路: 寄生式继承 与 原型式继承 很接近, 背后的思路类似于 寄生构造函数模式 和 工厂模式, 通过创建一个实现继承的 函数, 以某种方式增强对象, 然后返回这个对象
  • 适用场景: 寄生式继承 同样适合主要关注对象, 而不在乎 类型 和 构造函数 的场景, 其中 Object.create() 函数不是 寄生式继承 所 必需的, 任何返回新对象的函数都可以在这里使用
  • 缺点: 通过 寄生式继承 给对象添加的函数, 难以被 复用

8.6 寄生式组合继承

  • 思路: 在 组合继承 的思想基础之上进行优化, 修改 子类原型 时不再直接创建父类的实例, 而是通过 寄生式继承 来 继承父类原型, 然后将返回的新对象作为子类原型
  • 优点: 使用 寄生式继承 来弥补 组合继承 的缺点, 设置子类原型时只会对父类原型进行拷贝, 而不是创建父类实例对象, 这样可以避免创建无用是属性、方法, 寄生式组合继承 可以算是 引用类型 继承的最佳模式

8.7 Class 继承

  • 思路: 使用 extends 关键字, 继承任何拥有 [[Construct]] 和 原型 的对象, 很大程度上, 这意味着不仅可以 继承 一个 类, 也可以 继承 普通的 构造函数 (向后兼容)
  • 优点: 上文提到的各种继承策略都有自己的问题, 也有相应的妥协, 通过 class 语法糖, 可以轻松实现继承, 避免代码显得非常 冗长 和 混乱

九、参考

大家好, 我是墨渊君, 如果您喜欢我的文章可以: