0%

继承

本篇将介绍8种继承方法

8种继承方法:

  • 原型链继承
  • 构造继承
  • 组合继承
  • 寄生组合继承
  • 原型式继承
  • 寄生继承
  • 混入式继承
  • class中的extends继承

继承:子类可以继承父类的所有功能,并且对这些功能进行拓展

1. 原型链继承

将子类的原型对象指向父类的实例

1 Child.prototype = new Parent()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent () {
this.name = 'Parent'
this.sex = 'boy'
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child () {
this.name = 'child'
}
Child.prototype = new Parent()

var child1 = new Child()
child1.getName() // child
console.log(child1) // Child { name: 'child' }

可以尝试自己画一下原型继承思维导图,如果不会,看一下原型,原型链

伪代码

1
Child.prototype = new Parent();
2. 为什么不是Child.prototype = Parent.prototype

上面很容易想到,将子类的原型对象指向父类的原型对象。其实,这样的问题在于:那你就只能拿到父类的原型链上的属性和方法,无法拿到父类构造函数上的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent () {
this.name = 'Parent'
this.sex = 'boy'
}
Parent.prototype.getSex = function () {
console.log(this.sex)
}
function Child () {
this.name = 'child'
}
Child.prototype = Parent.prototype

var child1 = new Child()
child1.getSex() // undefined
console.log(child1) // Child { name: 'child'}

上面就是为什么要Child.prototype = new Parent() 而不是Child.prototype = Parent.prototype

3. 原型链继承的优缺点
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 Parent (name) {
this.name = name
this.sex = 'boy'
this.colors = ['white', 'black']
}
function Child () {
this.feature = ['cute']
}
var parent = new Parent('parent')
Child.prototype = parent

var child1 = new Child('child1')
child1.sex = 'girl'
child1.colors.push('yellow') // 如果执行child1.color = ['yellow']这样和上一句一样,不应该父类。这里是改变的原型对象的属性,会影响到所有用它的子类
child1.feature.push('sunshine') // 构造函数,this定义的属性和方法定义在实例对象上。feature它是属于child1实例自身的属性,它添加还是减少都不会影响到其他实例

var child2 = new Child('child2')

console.log(child1) // Child { feature: ['cute', 'sunshine'], sex: 'girl'}
console.log(child2) // Child { feature: ['cute']}

console.log(child1.name) // parent child1.name是原型对象Parent上的name,也就是'parent',虽然new Child()传递了child1,但是无效,接受参数的是Parent
console.log(child2.colors) // ['white', 'black', 'yellow']

console.log(parent) // Parent {name: 'parent', sex: 'boy', color: ['white', 'black', 'yellow']}

总结优点缺点:

  1. 优点:
  • 继承了父类的模版,又继承了父类的原型对象
  1. 缺点
  • 如果要给子类的原型上新增属性和方法,就必须放在Child.prototyp = new Parent()后面
  • 无法实现多继承(因为已经指定了原型对象)
  • 来自原型对象的所有属性都被共享,如果不小心修改了原型对象的引用类型的属性,那么所有子类创建的实例对象都会收到影响
  • 创建子类时,无法向父类构造函数传参数

2. instanceof

1.instanceof

官方简介:用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上面

1
2
3
4
5
6
7
8
9
10
11
12
function Parent () {
this.name = 'parent'
}
function Child () {
this.sex = 'boy'
}
Child.prototype = new Parent()
var child1 = new Child()

console.log(child1 instanceof Child) // true
console.log(child1 instanceof Parent) // true
console.log(child1 instanceof Object) // true
2. isPrototypeOf()

定义:判断指定对象object1是否存在于另一个对象object2的原型链中,是则返回true, 否则false

1
2
3
4
5
6
7
8
9
10
11
12
function Parent () {
this.name = 'parent'
}
function Child () {
this.sex = 'boy'
}
Child.prototype = new Parent()
var child1 = new Child()

console.log(Child.prototype.isPrototypeOf(child1)) // true
console.log(Parent.prototype.isPrototypeOf(child1)) // true
console.log(Object.prototype.isPrototypeOf(child1)) // true

判断方法只要把原型链继承instanceof查找思维导图反过来即可

3. 构造继承

定义:在子类构造函数内部使用call或者apply来调用父类构造函数

复习call, apply

  • 通过call(), apply(), bind()直接指定this的绑定对象,比如foo.call(obj)
  • 使用call(), apply()函数是会直接执行的
  • bind()是创建一个新的函数,需要手动调用
  • call(), apply() 用法基本一样,不过call接受若干参数,而apply接受的是一个数组
1. 基本原理
1
2
3
4
5
6
7
8
9
function Parent (name) {
this.name = name
}
function Child () {
this.sex = 'boy'
Parent.call(this, 'child')
}
var child1 = new Child()
console.log(child1) // Child { name: 'child', sex: 'boy' }

用伪代码来表示这个原理

1
2
3
function Child () {
Parent.call(this, ...arguments);
}
2. 重复属性
1
2
3
4
5
6
7
8
9
10
function Parent (name) {
this.name = name
}
function Child () {
this.sex = 'boy'
Parent.call(this, 'good boy')
this.name = 'bad boy'
}
var child1 = new Child()
console.log(child1) // Child {sex: "boy", name: "bad boy"}
3. 优点

解决了原型链继承的子类共享父类引用对象类型的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent (name, sex) {
this.name = name
this.sex = sex
this.colors = ['white', 'black']
}
function Child (name, sex) {
Parent.call(this, name, sex)
}
var child1 = new Child('child1', 'boy')
child1.colors.push('yellow')

var child2 = new Child('child2', 'girl')
console.log(child1) // Child { name: 'child1', sex: 'boy', colors: ['white', 'black', 'yellow']}
console.log(child2) // Child { name: 'child2', sex: 'girl', color: ['white', 'black']}

使用父类的构造函数来增强子类实例,等于复制了父类的实例给了子类,而且是深拷贝

总结优点:

  • 解决了原型链继承中子类实例共享父类引用对象的问题
  • 实现多继承
  • 创建子类时候,可以向父类传递参数
4. 缺点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child () {
this.sex = 'boy'
Parent.call(this, 'good boy')
}
Child.prototype.getSex = function () {
console.log(this.sex)
}
var child1 = new Child()
console.log(child1) // Child { sex: 'boy', name: 'good boy'}
child1.getSex() // boy
child1.getName() // Uncaught TypeError: child1.getName is not a function

缺点: 子类只能继承父类实例对象的属性和方法,不能继承父类原型的属性和方法

4. 注意点

实例并不是父类的实例,只是子类的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
function Parent (name) {
this.name = name
}
function Child () {
this.sex = 'boy'
Parent.call(this, 'child')
}
var child1 = new Child()

console.log(child1) // Child {sex: 'boy', name: 'child'}
console.log(child1 instanceof Child) // true
console.log(child1 instanceof Parent) // false
console.log(child1 instanceof Object) // true
5. 总结

优点:

  • 解决了原型链继承中的子类共享父类引用对象的问题,实现多继承,创建子类时,可以向父类传递参数

缺点:

  • 子类只能继承父类实例的属性和方法,不能继承父类原型的属性和方法
  • 实例只是子类的实例,并不是父类的实例
  • 无法实现函数复用,所有子类都有父类实例函数的副本,影响性能

父类的构造函数的某一个函数可能是功能函数,无论被复制多少份,输出的结果或者功能都是一样的,那么这类函数是完全可以拿来复用的。但是现在用了构造函数继承,完全复制了父类构造函数的方法和属性,这样产生的每个实例都有各自的方法,可是有的方法完全没有必要复制,可以用来公用的

4. 组合继承

既然原型链继承和构造继承各自有各自的优点缺点,那为什么不把它们组合一下

1
2
3
4
5
Child.prototype = new Parent();

function Child () {
Parent.call(this, ...arguments)
}

组合继承的概念:将原型链继承和构造函数继承组合在一起,发挥两者之场的一种继承模式

思路:

  • 使用原型链继承来保证子类能够继承到父类原型中的属性和方法
  • 使用构造函数继承来保证子类能够继承父类实例的属性和方法

基操:

  • 通过call, apply在子类构造函数调用父类构造函数
  • 将子类构造函数的原型对象指向父类构造函数创建的匿名实例
  • 将子类构造函数的原型对象的constructor指向子类构造函数
1. 理解使用
1
2
3
4
5
6
7
8
var child1 = new Child('child1')
var parent1 = new Parent('parent1')
console.log(child1) // Child{ name: 'child1', sex: 'boy' }
console.log(parent1)// Parent{ name: 'parent1' }
child1.getName() // 'child1'
child1.getSex() // 'boy'
parent1.getName() // 'parent1'
parent1.getSex() // Uncaught TypeError: parent1.getSex is not a function

为了得到上面的结果,我们写了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent (name) {
this.name = name;
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = 'boy';
Parent.call(this, name);
}
Child.prototype = new Parent();

Child.prototype.getSex = function () {
console.log(this.sex);
}
2. 理解constructor作用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = 'boy'
Parent.call(this, name)
}
Child.prototype = new Parent()
Child.prototype.getSex = function () {
console.log(this.sex)
}

var child1 = new Child('child1')
var parent1 = new Parent('parent1')
console.log(child1.constructor) // f Parent() {}
console.log(parent1.constructor) // f Parent() {}

constructor它不过是给我们一个提示,用来标识实例对象是由哪个构造函数创建的

上面看起来child1.constructor应该是Child, 但是却是Parent, 看起来不符合常理,所以才有了

1
Child.prototype.constructor = Child

来修复constructor指向

为什么要在组合继承里面修复constructor, 而在原型链继承没有修复,这个取决于你自己,constructor实际并没有什么作用,面试被问到constructor时候要知道它的指向

总结:

  • constructor是构造函数原型对象的一个属性,正常情况下指向构造函数
  • 它并不会影响任何JS内部属性,只是用来标识某一个实例是由哪个构造函数产生的
  • 如果我们因为原型链继承或者组合继承无意间修改了constructor指向,出于编程习惯,我们最好将它修改为正确的构造函数
3. constructor使用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
var a;
(function () {
function A () {
this.a = 1
this.b = 2
}
A.prototype.logA = function () {
console.log(this.a)
}
a = new A()
})()

a.logA() // 1

接下来我们需要在匿名函数A外面给构造函数的原型对象添加一个logB用以打印b

第一反应是

1
2
3
A.prototype.logB = function () {
console.log(this.b);
}

但是我们在外层访问不到A,但是我们可以访问到a, 我们可以通过a找到A

  1. 通过a.__proto__来访问到原型对象:

    1
    2
    3
    4
    a.__proto__.logB = function () {
    console.log(this.b);
    }
    a.logB();
  2. a.constructor.prototype

    1
    2
    3
    4
    a.constructor.prototype.logB = function () {
    console.log(this.b);
    }
    a.logB();

    虽然a实例上没有constructor,但是原型对象上有,所以a.constructor实际上拿的是原型对象上的constructor

4. 优点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Parent (name, colors) {
this.name = name
this.colors = colors
}
Parent.prototype.features = ['cute']
function Child (name, colors) {
this.sex = 'boy'
Parent.apply(this, [name, colors])
}
Child.prototype = new Parent()
Child.prototype.constructor = Child

var child1 = new Child('child1', ['white'])
child1.colors.push('yellow')
child1.features.push('sunshine')
var child2 = new Child('child2', ['black'])

console.log(child1) // { sex: 'boy', name: 'child1', colors: ['white', 'yellow']}
console.log(child2) // { sex: 'boy', name: 'child2', color: ['black']}
console.log(Child.prototype) // Parent { name: undefined, colors: undefined, constructor: f Child () {}}

console.log(child1 instanceof Child) // ture
console.log(child1 instanceof Parent) // true

总结:

  • 可以继承父类实例属性和方法,也可以继承父类原型属性和方法
  • 弥补了原型链继承中引用属性共享的问题
  • 可以传参,可以复用
5. 缺点
1
2
3
4
5
6
7
8
9
10
11
12
function Parent (name) {
console.log(name) // 这里有个console.log()
this.name = name
}
function Child (name) {
Parent.call(this, name)
}
Child.prototype = new Parent()
var child1 = new Child('child1')

console.log(child1)
console.log(Child.prototype)

打印结果:

1
2
3
4
undefined
child1
Child {name: 'child'}
Parent { name: undefined }

缺点:

  • 使用组合继承时,父类构造函数会被调用两次
  • 生成了两个实例,子类实例的属性和方法会覆盖父类原型(父类实例)的属性和方法,所以增加了不必要的内存
6. 实例对象上的引用类型和原型对象上的引用类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Parent (name, colors) {
this.name = name
this.colors = colors
}
Parent.prototype.features = ['cute']
function Child (name, colors) {
Parent.apply(this, [name, colors])
}
Child.prototype = new Parent()
Child.prototype.constructor = Child

var child1 = new Child('child1', ['white'])
child1.colors.push('yellow')
child1.features.push('sunshine')
var child2 = new Child('child2', ['black'])

console.log(child1.colors) // ['white', 'yellow']
console.log(child2.colors) // ['black'] 这里的colors是匿名实例中引用类型
console.log(child1.features) // ['cute','sunshine']
console.log(child2.features) // ['cute', 'sunshine'] // features是原型对象上的引用类型
7. 总结
实现方式:
  • 使用原型链继承来保证子类能够继承到父类的原型对象上的属性和方法
  • 使用构造函数继承来保证子类能继承到父类实例上的属性和方法
优点:
  • 可以继承父类实例属性和方法,也可以继承父类原型对象的属性和方法
  • 弥补了原型链继承中引用属性共享的问题
  • 可传参,可复用
缺点:
  • 使用组合继承时,父类构造函数会被调用两次
  • 生成了两个实例,子类实例上的属性和方法会覆盖父类实例(原型对象)上的属性和方法,增加了不必要的内存
constructor总结:
  • constructor是构造函数的原型对象里面的一个属性,指向的是构造函数
  • 它不会影响任何JS属性,只是用来标识某一个实例是哪个构造函数生成的
  • 如果我们因为使用原型链继承或者组合继承,无意间修改了constructor的指向,那么出于编程习惯,我们最好将它修改为正确的构造函数

5. 寄生组合继承

根据组合的缺点,我们不想要父类实例上的属性和方法,只想要继承父类原型链上的属性
那就是原型链继承对不对,Child.prototype = new Parent(); 但是,我们不想要父类实例上的属性和方法。

也就是,我们需要一个干净的实例对象,来作为子类的原型,而且这个干净的实例对象还得继承父类原型对象的属性

Object.create()

1
Object.create(proto, propertiesObject)

参数一:需要指定的原型对象
参数二:可选参数,给新对象添加新属性和描述器

proto指定你要创建的对象它的原型对象是谁
比如var parent1 = new Parent(),创建了一个对象parent1,parent1__proto__就是Parent.prototype
var obj = new Object(), 创建了一个对象obj, obj__proto__就是Object.prototype

1. 理解用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = 'boy'
Parent.call(this, name)
}
// 与组合继承的区别
Child.prototype = Object.create(Parent.prototype)

var child1 = new Child('child1')

console.log(child1) // Child { sex: 'boy', name: 'child1'}
child1.getName() // child1

console.log(child1.__proto__) // Parent {}
console.log(Object.create(null)) // {}
console.log(new Object()) // {}
2. 加深理解寄生组合继承和组合继承
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 Parent (name) {
this.name = name;
this.face = 'cry';
this.colors = ['white', 'black'];
}
Parent.prototype.features = ['cute'];
Parent.prototype.getFeatures = function () {
console.log(this.features);
}

function Child (name) {
Parent.call(this, name);
this.sex = 'boy';
this.face = 'smile';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

var child1 = new Child('child1');
child1.colors.push('yellow');
var child2 = new Child('child2');
child2.features = ['sunshine'];

console.log(child1); // Child { name: 'child1', face: 'smile', colors: ['white', 'black', 'yellow'], sex: 'boy'}
console.log(child2); // Child { name: 'child1', face: 'smile', colors: ['white', 'black'], sex: 'boy', features: 'sunshine'}
child1.getFeatures(); // ['cute']
child2.getFeatures(); // ['sunshine']
3. 总结

寄生组合继承算是ES6之前的一种比较完美的继承方式吧

它避免了组合继承调用两次父类构造函数,初始化两次实例属性的缺点

所以它拥有上诉所有继承方法的优点:

  • 只调用了一次父类构造函数,只创建了一份父类属性
  • 子类可以用到父类原型链上的属性和方法
  • 能够正常使用instanceof和isPrototypeOf方法

6. 原型式继承

原理: 创建一个构造函数,构造函数的原型指向对象,然后用new操作符创建实例,并且返回这个实例,本质是一个浅拷贝

1
2
3
4
5
6
function Object(obj) {
function F () {}
F.prototype = obj
F.prototype.constructor = F;
return new F();
}
1. 理解
1
2
3
4
5
6
7
8
9
10
11
12
13
var cat = {
heart: '❤️',
colors: ['white', 'black']
}

var guaiguai = Object.create(cat)
var huaihuai = Object.create(cat)

console.log(guaiguai) // {}
console.log(huaihuai) // {}

console.log(guaiguai.heart) // ❤️ guaiguai是一个__proto__指向cat的空对象
console.log(huaihuai.colors) // ['white', 'black']
2. 理解

上面的这种方法就是原型式继承,只是ES6之前没有Object.create()方法,所以用开头的伪代码代替它

改造上面的代码,我们自己实现一个Object.create方法

Object.create()的作用:

  • 它接受一个对象
  • 返回的是一个新对象
  • 新对象的原型链上必须找到传进来的对象
1
2
3
4
5
6
function create (obj) {
function F () {}
F.prototype = obj;
F.prototype.constructor = F;
return new F();
}

改编上面题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function create (obj) {
function F () {};
F.prototype = obj;
F.prototype.constructor = F;
return new F();
}
var cat = {
heart: '❤️',
colors: ['white', 'black']
}

var guaiguai = create(cat)
var huaihuai = create(cat)

console.log(guaiguai)
console.log(huaihuai)

console.log(guaiguai.heart)
console.log(huaihuai.colors)

既然是新对象的原型链中必须能找到传进来的对象,那还可以这样啊

1
2
3
4
5
function create (obj) {
const newObj = {};
newObj.__proto__ = obj;
return newObj;
}

但是我们模拟Object.crate()只是因为还没有它,那时候也还没有proto

3.总结
实现方法

该方法的原理就是创建一个构造函数,构造函数的原型对象指向传入的对象,然后使用new操作符创建一个实例,然后返回这个实例,本质是一个浅拷贝

ES5可以直接使用Oject.create()方法实现

优点
  • 在不用创建构造函数的情况下,实现了原型链继承,代码少了一部分
缺点
  • 一些引用数据的操作会出问题,两个实例会公用继承实例的引用数据类型
  • 谨慎定义方法,以免定义方法和继承对象原型的方法名重名
  • 无法直接给父级构造函数传参

7. 寄生式继承

原理:在原型式继承的基础上再封装一层,来增强这个对象,之后将这个对象返回

1. 了解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var cat = {
heart: '❤️',
colors: ['white', 'black']
}
function createAnother (original) {
var clone = Object.create(original);
clone.actingCute = function () {
console.log('我是一只会卖萌的猫咪')
}
return clone;
}
var guaiguai = createAnother(cat)
var huaihuai = Object.create(cat)

guaiguai.actingCute() // 我是一只会卖萌的猫咪
console.log(guaiguai.heart) // ❤️
console.log(huaihuai.colors) // ['white', 'black']
console.log(guaiguai) // Cat { actingCute: function() {} }
console.log(huaihuai) // {}
2. 总结
实现方式:
  • 在原型式继承的基础上再封装一层,来增强这个对象,再将它返回
优点:
  • 在不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分
缺点:
  • 一些引用数据的操作时会出现问题,两个实例公用继承实例的引用数据
  • 谨慎定义方法,以免定义方法与继承对象原型的方法重名
  • 无法直接给父级构造函数使用参数

8. 混入式继承

这个混入式方法继承很好玩,之前我们都是一个子类继承一个父类,而混入方式继承是一个子类继承多个父类

这里我们需要用到ES6的方法Object.assign();

它的作用就是把多个对象的属性和方法拷贝到目标对象,若存在同名属性,后面的会覆盖前面的,这是浅拷贝

伪代码来表示上面的:

1
2
3
4
5
6
7
function Child() {
Parent.call(this)
OtherParent.call(this)
}
Child.prototype = Object.create(Parent.prototype);
Object.assign(Child.prototype, OtherParent.prototype);
Child.prototype.constructor = Child;
1. 理解
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 Parent (sex) {
this.sex = sex
}
Parent.prototype.getSex = function () {
console.log(this.sex)
}
function OtherParent (colors) {
this.colors = colors
}
OtherParent.prototype.getColors = function () {
console.log(this.colors)
}
function Child (sex, colors) {
Parent.call(this, sex)
OtherParent.call(this, colors) // 新增的父类
this.name = 'child'
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype) // 新增的父类原型对象
Child.prototype.constructor = Child

var child1 = new Child('boy', ['white'])
child1.getSex() // boy
child1.getColors() // ['white']
console.log(child1) // Child { sex: 'boy', color: ['white'], name: 'child'}

现在child1不仅复制了Parent上的属性和方法,还复制了OtherParent上的

2. 理解原型链结构
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 Parent (sex) {
this.sex = sex
}
Parent.prototype.getSex = function () {
console.log(this.sex)
}
function OtherParent (colors) {
this.colors = colors
}
OtherParent.prototype.getColors = function () {
console.log(this.colors)
}
function Child (sex, colors) {
Parent.call(this, sex)
OtherParent.call(this, colors) // 新增的父类
this.name = 'child'
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype) // 新增的父类原型对象
Child.prototype.constructor = Child

var child1 = new Child('boy', ['white'])
// child1.getSex()
// child1.getColors()
// console.log(child1)

console.log(Child.prototype.__proto__ === Parent.prototype) // true
console.log(Child.prototype.__proto__ === OtherParent.prototype) // false
console.log(child1 instanceof Parent) // true
console.log(child1 instanceof OtherParent) // false
  • Child使用call,apply来复制OtherParent实例上的属性和方法
  • Child使用Object.assign()浅拷贝OtherParent.prototype上的属性和方法

9. class中的继承

在class继承主要依靠两个东西:

  • extends
  • super

该继承的效果和我们之前介绍的寄生组合继承的效果一样

1. 理解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Parent {
constructor(name) {
this.name = name;
}
getName () {
console.log(this.name);
}
}

class Child extends Parent {
constructor(name) {
super(name);
this.sex = 'boy';
}
}
var child1 = new Child('child1')
console.log(child1) // Child { name: 'child1', sex: 'boy'}
child1.getName() // child1

console.log(child1 instanceof Child) // true
console.log(child1 instanceof Parent) // ture

用寄生组合继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = 'boy'
Parent.call(this, name)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

var child1 = new Child('child1')
console.log(child1) // Child { name: 'child1', sex: 'boy'}
child1.getName() // child1

console.log(child1 instanceof Child) // true
console.log(child1 instanceof Parent) // ture
2. extends作用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Parent {
constructor (name) {
this.name = name
}
getName () {
console.log(this.name)
}
}
class Child extends Parent {
// constructor (name) {
// super(name)
// this.sex = 'boy'
// }
sex = 'boy' // 实例属性sex放到外面来
}
var child1 = new Child('child1')
console.log(child1)
child1.getName()

上面将super(name)去掉,结果一样,所以

1
2
3
4
5
6
7
8
class Child extends Parent{}
// 等同于

class Child extends Parent{
constructor(...args) {
super(...args);
}
}

class没有定义constructor方法,这个方法会被默认添加,这里我们没有使用constructor,它其实已经被隐式的添加和调用了

所以extends的作用:

  • class通过关键字extends继承父类的所有属性和方法
  • 若是使用了extends实现继承的子类内部没有constructor方法,则会被默认添加constructor和super
3. super作用

上面看来constructor可有可无,那super到底有什么用呢

1
2
3
4
5
6
7
8
9
10
11
12
13
class Parent {
constructor () {
this.name = 'parent'
}
}
class Child extends Parent {
constructor () {
// super(name) // 把super隐去
}
}
var child1 = new Child()
console.log(child1) // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor at new Child
child1.getName()

大概意思就是,你必须在constructor中调用一个super函数
这样,constructor和super就是一对啊

  • ES5中的继承(构造继承和寄生组合继承),实际上是先创建子类的实例对象this,再将父类的属性和方法添加到this上去(使用Parent.call(this))
  • ES6中,实际上先创建父类的实例对象this,也就是使用super(),然后再用子类的构造函数去修改this

通俗理解:子类必须在constructor里面调用super(),否则就会报错,因为子类没有自己的this对象,而是继承父类的this对象,然后对其加工,如果不调用super就得不到this对象

4. super当作函数调用

super被当作函数调用时,代表这父类的构造函数,但是返回的确实子类的实例,也就是super内的this指向Child

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Parent {
constructor () {
console.log(new.target.name)
}
}
class Child extends Parent {
constructor () {
var instance = super()
console.log(instance)
console.log(instance === this)
}
}
var child1 = new Child() // Child

var parent1 = new Parent()

console.log(child1)
console.log(parent1)

打印结果

1
2
3
4
5
6
7
8
9
10
'Child'

Child
true

'Parent'

Child {}
Parent {}

总结:

  • super当函数调用时,代表父类的构造函数,且返回子类的实例,此时也就是super内的this指向子类
  • 在子类constructor中super()相当于Parent.constructor.call(this)
5. super当作函数调用的限制

既然super被当作函数调用就相当于用call来改变父类构造函数的this指向,那有什么限制呢

  • 子类constructor如果要使用this的话必须放在super()后面
  • super当作函数调用时,只能在子类的constructor里使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Parent {
constructor (name) {
this.name = name
}
}
class Child extends Parent {
constructor (name) {
this.sex = 'boy'
super(name)
}
}
var child1 = new Child('child1')
console.log(child1)
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor at new Child
6. super被当作对象使用

super如果当成一个对象来使用的话,那就可能存在class中的不同地方

我们只要记住:

  • 在子类的普通函数中,super指向父类的原型对象
  • 在子类的静态方法中,super指向父类
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
class Parent {
constructor (name) {
this.name = name
}
getName () {
console.log(this.name)
}
}
Parent.prototype.getSex = function () {
console.log('boy')
}
Parent.getColors = function () {
console.log(['white'])
}
class Child extends Parent {
constructor (name) {
super(name)
super.getName()
}
instanceFn () {
super.getSex()
}
static staticFn () {
super.getColors()
}
}
var child1 = new Child('child1') // child1
child1.instanceFn() // 'boy'
Child.staticFn() // ['white']
console.log(child1) // Child { name: 'child1'}
7. super当成对象调用父类方法时this指向

上面var child1 = new Child(‘child1’);这里的super.getName()打印的不是child1吧,super这里指向的父类原型对象,那getName打印的是父类原型对象的name,那就是undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Parent {
constructor () {}
}
Parent.prototype.sex = 'boy'
Parent.prototype.getSex = function () {
console.log(this.sex)
}
class Child extends Parent {
constructor () {
super()
this.sex = 'girl'
super.getSex()
}
}
var child1 = new Child() // girl
console.log(child1) // Child { sex: 'girl'}

结果好像和我们想的不一样,getSex打印的是子类的属性值

所以,其实:

  • ES6规定,通过super调用父类的方法时,super会绑定子类的this

也就是

1
2
3
super.getSex()
// 即
Parent.prototype.getSex.call(this);

super还有一个特性,就是你在使用它时,必须显示的指定它是作为函数使用还是对象来使用,否则就会报错

1
2
3
4
5
6
7
8
class Child extends Parent {
constructor () {
super(); // 不报错
super.getSex(); // 不报错

console.log(super); // 这里会报错
}
}
8. 了解extends的继承目标

extends后面接着的继承目标不一定是一个class

class B extends A {} 只要A中有一个prototype属性的函数,就能被B继承

由于函数都有prototype属性,因此A可以是任意函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent () {
this.name = 'parent'
}
class Child1 extends Parent {}
class Child2 {}
class Child3 extends Array {}
var child1 = new Child1()
var child2 = new Child2()
var child3 = new Child3()
child3[0] = 1

console.log(child1) // Child1 { name: 'parent' }
console.log(child2) // Child2 {}
console.log(child3) // Child3 [1]
9. 总结
ES6中的继承:
  • 主要依赖extends关键字来实现继承,且继承的效果类似于寄生组合继承
  • 使用了extends实现继承,不一定要constructor和super,因为没有的话会默认产生并调用它们
  • extends后面接的目标不一定是class,只要是一个有prototype属性的函数就可以了
super相关:
  • 在实现继承时,如果子类有constructor函数,就必须在constructor里面调用super函数,因为它就是用来产生实例this的
  • super有两种调用方式:当成函数调用和当成对象来调用
  • super当成函数调用时,代表父类的构造函数,且返回子类的实例。也就是此时super里面的this指向子类的实例。在子类的constructor里super就相当于Parent.constructor.call(this)
  • super当成对象调用时,普通函数super对象指向父类的原型对象,静态函数中指向父类。通过super调用父类的方法时,super会绑定子类的this,相当于是Parent.prototype.fn.call(this)
ES5继承和ES6继承的区别:

区别:

  • ES5中的继承(如构造继承和寄生组合继承),实际上先创建子类的实例对象this,然后将父类的属性和方法添加到this上(Parent.call(this))
  • ES6继承,实际上是先创建父类的实例对象this(也就是super()),然后再用子类的构造函数去修改this