什么? chatgpt 居然告诉我 JS 中常见的继承方案有十种?
引言
继承 是面向对象编程中讨论最多的话题, 在 JS 中 继承 主要是通过 原型、原型链 实现的, 为了了解更多关于 JS 继承 的细节, 我问了 chatgpt 如下问题:

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 实例的一个 原型链

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

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

1.3 实例和原型的关系
在 JS 中我们有 两种 方式可以来判断 原型 是否存在于某个 实例 的 原型链 上
- 使用
instanceof运算符, 可检测构造函数的prototype属性是否出现在实例对象的原型链上,
child instanceof Child // Child.prototype 是否在 child 原型链上 => true
child instanceof Parent // Parent.prototype 是否在 child 原型链上 => true
child instanceof Object // Object.prototype 是否在 child 原型链上 => true
- 使用
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 二个缺点
原型中包含引用值的问题: 如果原型中包含了引用值, 那么这个值会在所有实例间进行共享, 如下代码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) // [ '北京', '上海', '杭州' ]
上面代码对应实例、构造函数、原型图如下, 其中绿色表示实例的 原型链

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

补充: 这也是为什么 属性 通常会在 构造函数 中定义, 而不会直接在 原型 上进行定义的原因
子类在实例化时不能给父类的构造函数传参: 如下代码,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 注意事项
- 为
原型添加方法或属性, 应该在原型设置之后再进行: 如下代码在设置原型前添加的属性或方法会丢失, 因为添加到初始原型对象上了, 后面修改了原型就导致这些属性、方法都丢失了
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
- 避免通过
字面量形式修改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
上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

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

2.2 两个优点
- 可解决上文提到的引用值问题: 每次执行
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 // ['北京', '上海']
上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

- 支持为父类构造函数传参: 如下代码, 在执行子类构造函数
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 两个缺点
- 不能共用属性、方法: 严格意义上可能并不算是继承, 有点像是构造函数
逻辑的抽离, 每次在实例化时都新建了属性、方法, 导致属性和方法都无法被共用, 但是实际上方法是应该允许被共用的, 才比较合理
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, 不是同一个方法
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
上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

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

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 });
上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

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

四、原型式继承
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
上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

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

补充: 原型式继承 非常适合 不需要 单独创建构造函数, 但仍然需要 在对象间 共享信息的场合, 比较适用的一个场景是, 基于现有的一个对象创建新的对象, 并进行适当的修改
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
上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

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

五、寄生式继承
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
上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

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

补充: 寄生式继承 同样适合主要关注对象, 而不在乎 类型 和 构造函数 的场景, 同时需要注意的是 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
上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

六、寄生式组合继承
在
组合继承中我们提到, 该继承方案是存在效率问题的, 在设置子类原型时会调用一次父类构造函数会创建一些无用的属性、方法, 然而本质上, 子类原型最终只要包含超类(父类)对象的所有实例属性即可, 同时子类构造函数只要在执行时重写自己的原型就行了
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
上面代码最终实例、构造函数、原型关系图如下, 其中绿色表示 原型链

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

6.2 优点
使用 寄生式继承 来弥补 组合继承 的缺点, 设置子类原型时只会对父类原型进行拷贝, 而不是创建父类实例对象, 这样可以避免创建无用是属性、方法, 寄生式组合继承 可以算是 引用类型 继承的最佳模式
七、Class 继承
上文提到的各种继承策略都有自己的问题, 也有相应的妥协; 正因为如此, 实现继承的代码也显得非常 冗长 和 混乱; 为解决这些问题 ES6 新引入的 class 关键字具有正式定义类的能力, 类(class) 是 ES6 中新的基础性语法糖结构, 虽然 class 从表面上看起来可以支持正式的面向对象编程, 但实际上它背后使用的 仍然 是 原型、原型链 和 构造函数 的概念
7.1 实现继承
使用 extends 关键字, 就可以继承任何拥有 [[Construct]] 和 原型 的对象, 很大程度上, 这意味着不仅可以 继承 一个 类, 也可以 继承 普通的 构造函数 (向后兼容)
- 继承
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
- 继承普通构造函数
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 没有显式支持多类继承, 但通过现有特性可以轻松地模拟这种行为
- 前置知识:
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' }
- 在实际开发中如果只是需要混入多个对象的属性, 则只需要使用
Object.assign(), 该方法就是为了混入对象行为而设计的
const obj = Object.assign({ name: 'moyuanjun' }, { age: 18 })
obj // { name: 'moyuanjun', age: 18 }
- 混入的方式有很多策略, 常见的策略是定义一组
可嵌套函数, 每个函数分别接收一个超类(父类)作为参数, 而将混入类定义为这个参数的子类, 并返回这个类, 这些组合函数可以连缀调用, 最终组合成超类(父类)表达式, 如下代码所示:
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
- 这里可以通过写一个辅助函数, 将嵌套调用展开, 如下代码新增了
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语法糖, 可以轻松实现继承, 避免代码显得非常冗长和混乱
九、参考
- 《JavaScript高级程序设计 (第4版)》
- 一文梳理JavaScript中常见的七大继承方案
- JavaScript常用八种继承方案
- js实现继承的最佳方案, 探索企业级的解决方案
- 一文梳理JavaScript中常见的七大继承方案
- 彻底搞懂JS原型、原型链和继承
本文首发于个人公众号 「昆仑虚 F2E」, 欢迎 👏🏻👏🏻 关注获取一手资讯
转载自:https://juejin.cn/post/7236387878006177829