细说JS继承

本文最后更新于:2021年2月1日 晚上

继承在各种编程语言中都扮演着一个重要的角色,应用场景也十分丰富。它是面向对象且主要作用就是复用代码,提高效率。借助JS这种弱类型语言的灵活特性,又让它显得更灵活飘逸,在前端基建中经常可以看到继承的使用。所以有必要捋一捋继承这一块的知识,主要是JS中继承的各种方式以及各自的优缺点,还有 ES6extend 的原理。

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
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() // react
vue.getName() // vue

看下结果:

从结果可以看到,这种原型式可以继承对象的属性和方法,但是缺陷也很明显,存在不同实例的引用类型内存空间共享的问题。看到这里,还挖掘出 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
react.getContent() // ['html', 'css', 'js', 'jsx']

从输出结果看到,实例 react 能同时继承 getNamegetContent 方法,不过缺陷也是和原型式继承一样,存在共享内存空间的问题,但是相对来说,寄生式继承可以在父类的基础上添加更多的方法。

寄生组合式继承

接下来再看另一种组合继承,寄生组合式继承,上面在说原型链与构造函数组合继承的时候,有讲到其中一个缺陷,就是需要调用两次父类的构造函数而造成额外的性能开销,那寄生组合式刚好可以解决这个问题,同时这也是目前所有继承方式里面较优的继承方案。

看下代码示例:

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的源码,其实原理也是使用了寄生组合式继承,这也证明了寄生组合式继承确实是一种比较优秀的继承方案。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!