本文最后更新于:2021年2月1日 晚上
继承在各种编程语言中都扮演着一个重要的角色,应用场景也十分丰富。它是面向对象且主要作用就是复用代码,提高效率。借助JS这种弱类型语言的灵活特性,又让它显得更灵活飘逸,在前端基建中经常可以看到继承的使用。所以有必要捋一捋继承这一块的知识,主要是JS中继承的各种方式以及各自的优缺点,还有 ES6
中 extend
的原理。
JS中的几种继承方式
原型链继承
原型链继承是比较常见的继承方式,其中会涉及到构造函数、原型和实例等概念,对这方面的概念还不清楚的,建议先看一下这方面的知识。
先看一下原型链继承的例子:
1 2 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。但是关于对象,大家都知道对象是会共享内存的,再加入以下几句代码:
1 2 3 4 5 6 7 8
| const vue = new Frame()
react.content.push('jsx')
console.log({ vue: vue, react: react })
|
结果应该不用多说,vue 实例的 content 属性里面也有了 jsx,所以原型链继承的缺点就是原型对象的内存空间是共享的,只要改变其中一个,另外的也会受到影响。
构造函数继承
构造函数继承需要借助call来实现:
1 2 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
,子类是没法继承的。
原型链与构造函数的组合继承
既然两种方式各有缺陷,那能不能组合在一起呢?答案肯定是可以的。
1 2 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
简单地说,就是用作新对象的原型对象。看例子:
1 2 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
的一个作用:可以拿来做浅拷贝使用。
寄生式继承
将浅拷贝到的对象,再通过添加一些方法增强其功能,这样的继承方式叫做寄生式继承。
看下示例:
1 2 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
方法,不过缺陷也是和原型式继承一样,存在共享内存空间的问题,但是相对来说,寄生式继承可以在父类的基础上添加更多的方法。
寄生组合式继承
接下来再看另一种组合继承,寄生组合式继承,上面在说原型链与构造函数组合继承的时候,有讲到其中一个缺陷,就是需要调用两次父类的构造函数而造成额外的性能开销,那寄生组合式刚好可以解决这个问题,同时这也是目前所有继承方式里面较优的继承方案。
看下代码示例:
1 2 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的源码,其实原理也是使用了寄生组合式继承,这也证明了寄生组合式继承确实是一种比较优秀的继承方案。