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

868 阅读21分钟

引言

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

image.png

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

一、原型链继承

1.1 基本思想

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

  • 定义了两个 类型 数据分别是 ParentChild
  • Parent 类型定义了 nameage 属性并且在 原型 上声明了方法 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

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

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. 混入的方式有很多策略, 常见的策略是定义一组 可嵌套 函数, 每个函数分别接收一个 超类(父类) 作为参数, 而将 混入类 定义为这个参数的子类, 并返回这个类, 这些组合函数可以连缀调用, 最终组合成 超类(父类) 表达式, 如下代码所示:
  • FooBarBaz 是一组函数, 接收一个 超类(父类), 函数内部继承于 超类(父类) 进行扩展, 返回一个新的子类
  • FooBarBaz 函数进行嵌套执行, 返回一个混合类 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 语法糖, 可以轻松实现继承, 避免代码显得非常 冗长混乱

九、参考

Group 3143