本文最后更新于:2021年2月1日 晚上
                
              
            
            
              继承在各种编程语言中都扮演着一个重要的角色,应用场景也十分丰富。它是面向对象且主要作用就是复用代码,提高效率。借助JS这种弱类型语言的灵活特性,又让它显得更灵活飘逸,在前端基建中经常可以看到继承的使用。所以有必要捋一捋继承这一块的知识,主要是JS中继承的各种方式以及各自的优缺点,还有 ES6 中 extend 的原理。
JS中的几种继承方式
原型链继承
原型链继承是比较常见的继承方式,其中会涉及到构造函数、原型和实例等概念,对这方面的概念还不清楚的,建议先看一下这方面的知识。
先看一下原型链继承的例子:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | function Fe() {this.name = 'fe'
 this.content = ['html', 'css', 'js']
 }
 
 function Frame() {
 this.name = 'fe frame'
 }
 
 Frame.prototype = new Fe()
 
 const react = new Frame()
 
 react.name = 'react'
 
 console.log({
 react: react
 })
 
 | 
输出结果:

先定义一个父类 Fe 表示前端的包含了 html 、css 和 js,再定义一个类为 Frame ,通过原型链继承 Fe ,可以看到 Frame 也有了 content 属性,表示应当支持 html 、css 和 js。但是关于对象,大家都知道对象是会共享内存的,再加入以下几句代码:
| 12
 3
 4
 5
 6
 7
 8
 
 | const vue = new Frame()
 react.content.push('jsx')
 
 console.log({
 vue: vue,
 react: react
 })
 
 | 
结果应该不用多说,vue 实例的 content 属性里面也有了 jsx,所以原型链继承的缺点就是原型对象的内存空间是共享的,只要改变其中一个,另外的也会受到影响。
构造函数继承
构造函数继承需要借助call来实现:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | function Fe() {this.name = 'fe'
 this.content = ['html', 'css', 'js']
 }
 
 Fe.prototype.getName = function() {
 return this.name
 }
 
 function Frame() {
 Fe.call(this)
 this.name = 'fe frame'
 }
 
 const react = new Frame()
 const vue = new Frame()
 react.content.push('jsx')
 vue.content.push('template')
 
 console.log({
 vue: vue,
 react: react
 })
 
 | 
查看输出结果:

可以看到,这一次,vue实例跟react实例的content是互不影响的,但是呢,这种继承也存在缺陷,看以下操作:

可以看到,构造函数继承的缺陷就是:父类在其原型对象上所添加的方法如 getName,子类是没法继承的。
原型链与构造函数的组合继承
既然两种方式各有缺陷,那能不能组合在一起呢?答案肯定是可以的。
| 12
 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
 
 | function Fe() {this.name = 'fe'
 this.content = ['html', 'css', 'js']
 }
 
 Fe.prototype.getName = function() {
 return this.name
 }
 
 function Frame() {
 Fe.call(this)
 this.name = 'fe frame'
 }
 
 Frame.prototype = new Fe()
 
 Frame.prototype.constructor = Frame
 
 const react = new Frame()
 const vue = new Frame()
 
 react.name = 'react'
 react.content.push('jsx')
 vue.name = 'vue'
 vue.content.push('template')
 
 console.log({
 vue: vue,
 react: react
 })
 
 | 
输出结果:

从输出结果可以看到,两个实例之间的 content 属性互不影响,同时也能继承父类原型对象的属性。但同时我们也看到了,这个过程中执行了两次 Fe,多产生了一次性能开销,这就是这个组合继承的缺陷。
以上是对于构造函数的继承方式,下面再接着看对于普通对象的继承方式。
原型式继承
这里需要用到ES5中的 Object.create 方法,看一下定义:
**Object.create()**方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
语法 Object.create(proto,[propertiesObject])
—— MDN
简单地说,就是用作新对象的原型对象。看例子:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | let frame = {name: 'frame',
 content: ['html', 'css', 'js'],
 getName: function() {
 return this.name;
 }
 }
 
 let react = Object.create(frame)
 let vue = Object.create(frame)
 
 react.name = 'react'
 react.content.push('jsx')
 
 vue.name = 'vue'
 vue.content.push('template')
 
 react.getName()
 vue.getName()
 
 | 
看下结果:

从结果可以看到,这种原型式可以继承对象的属性和方法,但是缺陷也很明显,存在不同实例的引用类型内存空间共享的问题。看到这里,还挖掘出 create 的一个作用:可以拿来做浅拷贝使用。
寄生式继承
将浅拷贝到的对象,再通过添加一些方法增强其功能,这样的继承方式叫做寄生式继承。
看下示例:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | let frame = {name: 'frame',
 content: ['html', 'css', 'js'],
 getName: function() {
 return this.name;
 }
 }
 
 function parasitic(parent) {
 let clone = Object.create(parent)
 clone.getContent = function() {
 return this.content
 }
 return clone
 }
 
 let react = parasitic(frame)
 react.name = 'react'
 react.content.push('jsx')
 
 react.getName()
 react.getContent()
 
 | 
从输出结果看到,实例 react 能同时继承 getName 和 getContent 方法,不过缺陷也是和原型式继承一样,存在共享内存空间的问题,但是相对来说,寄生式继承可以在父类的基础上添加更多的方法。
寄生组合式继承
接下来再看另一种组合继承,寄生组合式继承,上面在说原型链与构造函数组合继承的时候,有讲到其中一个缺陷,就是需要调用两次父类的构造函数而造成额外的性能开销,那寄生组合式刚好可以解决这个问题,同时这也是目前所有继承方式里面较优的继承方案。
看下代码示例:
| 12
 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
 
 | function parasitic(parent, child) {child.prototype = Object.create(parent.prototype)
 child.prototype.constructor = child
 }
 
 function Frame() {
 this.name = 'frame'
 this.content = ['html', 'css', 'js']
 }
 
 Frame.prototype.getContent = function () {
 return this.content
 }
 
 function React() {
 Frame.call(this)
 this.name = 'react'
 this.feature = 'jsx'
 }
 
 parasitic(Frame, React)
 
 React.prototype.getFeature = function() {
 return this.feature
 }
 
 const react = new React()
 
 | 
在控制台操作一波,看下结果:

通过结果,可以看到无论是属性还是方法,寄生组合式都可以很好的继承,且相对于第一种组合继承,它还可以减少构造函数调用次数,减少性能开销,是上面讲到的6种继承方式里最优的一种。
但熟悉 JS 的朋友们都知道,ES6还提供了一个关键词 extends,这也是继承方式的一种,那它的底层原理又是怎么实现的呢?
ES6的extends关键字
有兴趣的朋友可以去看下通过 babel 转译出来的 extends的源码,其实原理也是使用了寄生组合式继承,这也证明了寄生组合式继承确实是一种比较优秀的继承方案。