继承

继承(inheritance)是面向对象软件技术当中的一个概念。

如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”

继承可以使子类获得父类的属性和方法,同时子类还可以创建自己的属性和方法或者重写覆盖父类的一些属性和方法,使其获得与父类不同的功能。

方法

下面给出JavaScripy常见的继承方式:

  • 原型链继承
  • 构造函数继承(借助 call)
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Parent1(name) {
this.name = name
this.body = ['头','手','脚']
}

function Child1(name) {
this.type = 'child'
}

Child1.prototype = new Parent1()

let p1 = new Parent1('小明')
let p2 = new Child1('小红1')
let p3 = new Child1('小红2')
p1.body.push('鼻子')
p2.body.push('眼睛')

console.log(p1.body) // 我叫小明,我有头、手、脚、鼻子
console.log(p2.body) // 我叫undefined,我有头、手、脚、眼睛
console.log(p3.body) // 我叫undefined,我有头、手、脚、眼睛

问题1:p2的body发生变化之后,p3的body也随着一起变化了,这是因为两个实例使用的是同一个原型对象,内存空间是共享的。

问题2:Child1无法继承Parent1的构造函数

构造函数继承

借助 call调用Parent函数

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
function Parent2(name) {
this.name = name
this.body = ['头', '手', '脚']
}

Parent2.prototype.show = function () {
return `我叫${this.name},我有${this.body.join('、')}`
}

function Child2(name) {
Parent2.call(this, name)
this.type = 'child'
}

let p4 = new Parent2('小明')
let p5 = new Child2('小红1')
let p6 = new Child2('小红2')
p4.body.push('鼻子')
p5.body.push('眼睛')

console.log(p4.show())// 我叫小明,我有头、手、脚、鼻子
console.log(p5.body) // ['头', '手', '脚', '眼睛']
console.log(p6.body) // ['头', '手', '脚']
console.log(p5.show()) // p5.show is not a function
console.log(p6.show()) // p6.show is not a function

这样解决了原型链继承中的构造函数赋值的问题以及new Child2创建的实例指向同一个原型对象,内存空间共享的问题。

同时也带来了一个新的问题,就是子类无法继承父类原型对象中方法

相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法。

组合继承

组合继承结合原型链继承构造函数继承的优点,将两种方法结合起来。

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
31
function Parent3(name) {
this.name = name
this.body = ['头', '手', '脚']
}

Parent3.prototype.show = function () {
return `我叫${this.name},我有${this.body.join('、')}`
}

function Child3(name) {
Parent3.call(this, name)
this.type = 'child'
}

Child3.prototype = new Parent3()
// 此时 Child3.prototype 中的 constructor 被重写了,会导致Child3.prototype.constructor === Parent3

// 所以在这里需要修改一下构造函数的指向
Child3.prototype.constructor = Child3

let p7 = new Parent3('小明')
let p8 = new Child3('小红1')
let p9 = new Child3('小红2')
p8.body.push('鼻子')
p9.body.push('眼睛')

console.log(p7.show()) // 我叫小明,我有头、手、脚
console.log(p8.show()) // 我叫小红1,我有头、手、脚、鼻子
console.log(p9.show()) // 我叫小红1,我有头、手、脚、鼻子

console.log(p8.constructor) // Child3

这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到Parent3 执行了两次(Parent3.call(this, name)Child3.prototype = new Parent3()),造成了额外的性能开销。

原型式继承

原理是主要借助Object.create方法实现普通对象的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let parent4 = {
name: '',
body: ['头', '手', '脚'],
show: function () {
return `我叫${this.name},我有${this.body.join('、')}`
}
}

let p10 = Object.create(parent4)
let p11 = Object.create(parent4)
p10.name = '小红1'
p10.body.push('鼻子')
p11.name = '小红2'
p11.body.push('眼睛')

console.log(p11.show()) // 我叫小红1,我有头、手、脚、鼻子、眼睛
console.log(p11.show()) // 我叫小红2,我有头、手、脚、鼻子、眼睛

这种继承方式的缺点也很明显,因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能。同时这种方式,无法为继承的对象统一添加方法。

寄生式继承

Object.create方法实现普通对象的继承的同时,可以添加一些新的方法

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
let parent5 = {
name: '',
body: ['头', '手', '脚'],
show: function () {
return `我叫${this.name},我有${this.body.join('、')}`
}
}

function clone(parent) {
let child = Object.create(parent)
child.hide = function() {
return `${this.name}啥也没有`
}
return child
}

let p12 = clone(parent5)
p12.name = '小红1'
p12.body.push('鼻子')

let p13 = clone(parent5)
p13.name = '小红2'
p13.body.push('眼睛')

console.log(p12.show()) // 我叫小红1,我有头、手、脚、鼻子、眼睛
console.log(p13.show()) // 我叫小红2,我有头、手、脚、鼻子、眼睛
console.log(p12.hide()) // 小红1啥也没有
console.log(p13.hide()) // 小红2啥也没有

确定就跟原型式继承一样。

寄生组合式继承

寄生组合式继承,借助解决普通对象的继承问题的Object.create 方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式

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
31
32
33
34
35
36
37
38
function Parent6(name) {
this.name = name
this.body = ['头', '手', '脚']
}

Parent6.prototype.show = function () {
return `我叫${this.name},我有${this.body.join('、')}`
}

function Child6(name) {
Parent6.call(this, name)
this.type = 'child'
}

function clone(parent, child) {
//通过 Object.create 可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype)
child.prototype.constructor = child
}

clone(Parent6, Child6)

Child6.prototype.hide = function () {
return `${this.name}啥也没有`
}

let p14 = new Parent6('小明')
let p15 = new Child6('小红1')
let p16 = new Child6('小红2')
p15.body.push('鼻子')
p16.body.push('眼睛')

console.log(p14.show()) // 我叫小明,我有头、手、脚
console.log(p15.show()) // 我叫小红1,我有头、手、脚、鼻子
console.log(p16.show()) // 我叫小红2,我有头、手、脚、眼睛

console.log(p15.hide()) // 小红1啥也没有
console.log(p16.hide()) // 小红2啥也没有

ES6(extends)

使用ES6 中的extends关键字直接实现 JavaScript的继承

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
31
32
33
34
class Parent7 {
constructor(name) {
this.name = name
this.body = ['头', '手', '脚']
}
show() {
return `我叫${this.name},我有${this.body.join('、')}`
}
}

class Child7 extends Parent7 {
constructor(name) {
//向父级的构造函数传参
super(name)
this.type = 'child'
}

hide() {
return `${this.name}啥也没有`
}
}

let p17 = new Parent6('小明')
let p18 = new Child6('小红1')
let p19 = new Child6('小红2')
p18.body.push('鼻子')
p19.body.push('眼睛')

console.log(p17.show()) // 我叫小明,我有头、手、脚
console.log(p18.show()) // 我叫小红1,我有头、手、脚、鼻子
console.log(p19.show()) // 我叫小红2,我有头、手、脚、眼睛

console.log(p18.hide()) // 小红1啥也没有
console.log(p19.hide()) // 小红2啥也没有