引言
继承
是面向对象编程中讨论最多的话题, 在 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」获取最新文章。
- GitHub: github.com/MoYuanJun
- 个人网站(昆仑虚, 虽然现在没啥东西): www.kunlunxu.cc