Loading

JS对象-继承

来自:
https://juejin.cn/post/6844904098941108232

目录:
1、原型链继承
2、构造继承
3、组合继承
4、寄生组合继承
5、原型继承
6、寄生继承
7、混入式继承
8、class中的extends继承

1.原型链继承

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

1.1题目一
(理解原型链继承的概念)

  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()
  console.log(child1)

答案:

  child
  Child { name: "child"}

解析:

  • child1是通过子类构造函数Child生成的对象,那就有属性name,并且属性值是child。
  • 然后子类构造函数Child它的原型指向了父类构造函数Parent创建出来的“无名实例”。
  • 这样的话,child1就可以使用这个“无名实例”里的所有属性和方法了,因此child1.getName()有效,并且打印出child。
  • 另外由于sex、getName都是Child原型对象上的属性,所以并不会表现在child1上。

所以现在你知道了吧,这种方式就叫做原型链继承。

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

我们来写个伪代码,方便记忆:

  Child.prototype = new Parent()

当然,更加严谨一点的做法其实还有一步:Child.prototype.constructor = Child,这里先卖个关子,到题目4.2中我们再来详细说它。

1.2题目二

不知道你们在看到原型链继承这个词语的时候,第一时间想到的是什么?
有没有和我一样,想到的是把子类的原型对象指向父类的原型对象的:

  Child.prototype = Parent.prototype

之后我就为我的想法感到惭愧。。

如果我只能拿到父类原型链上的属性和方法那也太费了吧,我可不止这样,我还想拿到父类构造函数上的属性。

所以这道题:

  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()
  console.log(child1)

结果为:

  undefined
  Child { name: 'child' }

你可以结合上面的那张图,自个脑补一下,child1它的原型链现在长啥样了。

解析:

  • child1上能使用的属性和方法只有name、getSex,所以getSex打印出的会是undefined
  • 打印出的child1只有name属性,getSex为原型上的方法所以并不会表现出来。

这道题是个错误的做法啊!我只是为了说明一下,为什么原型链继承是要用Child.prototype = new Parent()这种方式。

1.3题目三
(理解原型链继承的优点和缺点)

这道题的结果大家能想到吗?

请注意对象是地址引用的哦。

  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.feature.push('sunshine')

  var child2 = new Child('child2')
  
  console.log(child1)
  console.log(child2)

  console.log(child1.name)
  console.log(child2.colors)

  console.log(parent)

答案:

  Child { feature: [ 'cute', 'sunshine'], sex: 'girl'}
  Child { feature: [ 'cute' ]}

  parent
  [ 'white', 'black', 'yellow']

  Parent { name: "parent", sex: 'boy', colors: [ 'white', 'black', 'yellow' ]}

解析:

  • child1在创建完之后,就设置了sex,并且给colors和features都push了新的内容。
  • child1.sex = 'girl'这段代码相当于是个child1这个实例对象新增了一个sex属性。相当于是:原本我是没有sex这个属性的,我想要获取就得拿原型对象parent上的sex,但是现在你加上一句child1.sex就等于是我自己也有了这个属性了,就不要你原型上的了,所以并不会影响到原型对象parent上。
  • 但是child1.colors这里,注意它的操作,它是直接使用了.push()的,也就是说我得先找到colors这个属性,发现实例对象parent上有,然后就拿来用了,之后执行push操作,所以这时候改变的是原型对象parent上的属性,会影响到后续所有的实例对象。(这里你会有疑问了,凭什么sex就是在实例对象child上新增,而我colors不行,那是因为操作的方式不同,sex那里是我不管你有没有,反正我就直接用=来覆盖了,可是push它的前提是我得先有colors且类型是数组才行,不然你换成没有的属性,比如一个名为clothes的属性,child1.clothes.push('jacket')它直接就报错了,如果你使用的是child1.colors = ['yellow']这样才不会影响parent)
  • 而feature它是属于child1实例自身的属性,它添加还是减少都不会影响到其他实例。
  • 因此child1打印出了feature和sex两个属性。(name和color属于原型对象上的属性并不会被表现出来)
  • child2没有做任何操作,所以它打印出的还是它自身的一个feature属性。
  • child1.name 是原型对象parent上的name,也就是'parent',虽然我们在new Child的时候传递了'child1', 但它显然是无效的,因为接收name属性的是构造函数Parent,而不是Child。
  • child2.colors由于用的也是原型对象parent上的colors,又由于之前被child1给改变了,所以打印出来的会是['white', 'black', 'yellow']
  • 将最后的原型对象parent打印出来,name和sex没变,colors却变了。

总结-原型链继承

现在我们就可以得出原型链继承它的优点和缺点了

优点:

  • 继承了父类的模板,又继承了父类的原型对象

缺点:

  • 如果要给子类的原型上新增属性和方法,就必须放在Child.prototype = new Parent()这样的语句后面。
  • 无法实现多继承(因为已经指定原型对象了)
  • 来自原型对象的所有属性都被共享了,这样如果不小心修改了原型对象中引用类型属性,那么所有子类创建的实例对象都会受到影响(这点从修改child1.colors可以看出来)
  • 创建子类时,无法向父类构造函数传参数(这点从child1.name可以看出来)。

2.instanceof

2.1题目一

这道题主要是想介绍一个重要的运算符:instanceof

先看看官方的简介:

instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。

再来看看通俗点的简介:

a instanceof B

实例对象a的原型链(proto)上是否有B.prototype,有则返回true,否则返回false。

上题吧:

  function Parent () {
    this.name = 'parent'
  }
  function Child () {
    this.sex = 'boy'
  }
  Child.prototype = new Parent()
  var child1 = new Child()

  console.log(child1 instanceof Child)
  console.log(child1 instanceof Parent)
  console.log(child1 instanceof Object)

结果为:

  true
  true
  true

这里就利用了前面提到的原型链继承,而且三个构造函数的原型对象都存在于child1的原型链上。

也就是说,左边的child1它会向它的原型链中不停的查找,看有没有右边那个构造函数的原型对象。

2.2题目二
(了解isPrototypeOf()的使用)
既然说到了instanceof,那么就不得不提一下isPrototypeOf这个方法了。
它属于Object.prototype上的方法,这点你可以将Object.prototype打印在控制台中看看。

isPrototypeOf()的用法和instanceof相反。

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

例如还是上面这道题,我们将要打印的内容改一下:

  function Parent() {
        this.name = 'parent'
  }
  function Child() {
        this.sex = 'boy'
  }
  Child.prototype = new Parent()
  var child1 = new Child()

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

这里输出的依然是三个true:

  true
  true
  true

3.构造继承

了解了最简单的原型链继承,在让我们来看看构造继承呀,也叫构造函数继承。

在子类构造函数的内部使用call或apply来调用父类构造函数

为了方便你查看,我们先来复习一波.call和apply方法。

  • 通过call()、apply()或者bind()方法直接指定this的绑定对象, 如foo.call(obj)
  • 使用.call()或者.apply()的函数是会直接执行的
  • 而bind()是创建一个新的函数,需要手动调用才会执行
  • .call()和.apply()用法基本类似,不过call接收若干个参数,而apply接收的是一个数组

3.1题目一
(构造继承的基本原理)

所以来看看这道题:

  function Parent(name) {
        this.name = name
  }
  function Child() {
        this.sex = 'boy'
        Parent.call(this, 'child')
  }
  var child1 = new Child()
  console.log(child1)

child1中会有哪些属性呢?
首先sex我们知道肯定会有的,毕竟它就是构造函数Child里的。
其次,我们使用了Parent.call(this, 'child'),.call函数刚刚已经说过了,它是会立即执行的,而这里又用了.call来改变Parent构造函数内的指向,所以我们是不是可以将它转化为伪代码:

  function Child() {
        this.sex = 'boy'
        // 伪代码
        this.name = 'child'
  }

你就理解为相当于是直接执行了Parent里的代码。使用父类的构造函数来增强子类实例,等于是复制父类的实例给子类。

所以构造继承的原理就是:

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

同样的,来写下伪代码:

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

arguments表示的是你可以往里面传递参数,当然这只是伪代码)

3.2题目二

如果你觉得上面这道题还不具有说明性,我们来看看这里。

现在我在子类和父类中都加上name这个属性,你觉得生出来的会是好孩子还是坏孩子呢?

  function Parent(name) {
        this.name = name
  }
  function Child() {
        this.sex = 'boy'
        Parent.call(this, 'good boy')
        this.name = 'bad boy'
  }
  var child = new Child()
  console.log(child1)

其实是好是坏很好区分,只要想想3.1里,把Parent.call(this, 'good boy')换成伪代码就知道了。

换成了伪代码之后,等于是重复定义了两个相同名称的属性,当然是后面的覆盖前面的啦。

所以结果为:

  Child { sex: 'boy', name: 'bad boy' }

3.3题目三

(构造继承的优点)

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

刚刚的题目都是一些基本数据类型,让我来加上引用类型看看

  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)
  console.log(child2)

这道题看着和1.3好像啊,没错,在父类构造函数中有一个叫colors的数组,它是地址引用的。

在原型链继承中我们知道,子类构造函数创建的实例是会查找到原型链上的colors的,而且改动它会影响到其它的实例,这是原型链继承的一大缺点。

而现在呢?你看看使用了构造继承,结果为:

  Child{ name: 'child1', sex: 'boy', colors: ['white', 'black', 'yellow'] }
  Child{ name: 'child2', sex: 'girl', colors: ['white', 'black'] }

我们发现修改child1.colors并不会影响到其它的实例。

这里的原因其实我们前面也说了:

使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类。

所以现在child1和child2现在分别有它们各自的colors了,就不共享了。

而且这种拷贝属于深拷贝,验证的方式是你可以把colors数组中的每一项改为一个对象,然后修改它看看。

因此我们可以得出构造继承的优点:

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

3.4题目四
(构造继承的缺点一)
在了解继承的时候,我们总是会想到原型链上的属性和方法能不能被继承到。
采用了这种构造继承的方式,能不能继承父类原型链上的属性呢?

来看下面这道题目:

  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)
  child1.getSex()
  child1.getName()

我给子类和父类的原型对象上都分别加了一个方法,然后调用它们。

结果是:

  Child {sex: "boy", name: "good boy"}
  boy
  Uncaught TypeError: child1.getName is not a function

所以我们可以看出构造集成的一个最大的缺点,那就是:

  • 构造继承只能继承父类的实例属性和方法,不能继承父类原型的属性和方法

3.5题目五
(构造继承的缺点二)
它的第二个缺点是:实例并不是父类的实例,只是子类的实例。

刚刚不是才学的一个叫instanceof的运算符吗?它就能检测某个实例的原型链上能不能找到构造函数的原型对象。

换句话说就能检测某个对象是不是某个构造函数的实例。

所以让我们来看看:

  function Parent(name) {
        this.name = name
  }
  function Child() {
        this.sex = 'boy'
        Parent.call(this, 'child')
  }
  var child1 = new Child()

  console.log(child1)
  console.log(child1 instanceof child)
  console.log(child1 instanceof Parent)
  console.log(child1 instanceof Object)

结果为:

  Child { sex: "boy", name: "child"}
  true
  false
  true
  • 第一个true很好理解啦,我就是你生的,你不true谁true
  • 第二个为false其实也很好理解啦,想想刚刚的5.3,我连你父类原型上的方法都不能用,那我和你可能也没有关系啦,我只不过是复制了你函数里的属性和方法而已。
  • 第三个true,必然的,实例的原型链如果没有发生改变的话最后都能找到Object.prototype啦。

(虽说构造继承出来的实例确实不是父类的实例,只是子类的实例。但我其实是不太明白教材中为什么要说它是一个缺点呢?鄙人愚昧,想的可能是:子类生成的实例既然能用到父类中的属性和方法,那我就应该也要确定这些属性和方法的来源,如果不能使用instanceof检测到你和父类有关系的话,那就会对这些凭空产生的属性和方法有所质疑...)

因此构造继承的第二个缺点是:

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

总结-构造继承
构造继承总结来说:

优点

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

缺点

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

(最后一个缺点的意思:父类构造函数中的某个函数可能只是一个功能型的函数,它不论被复制了多少份,输出的结果或者功能都是一样的,那么这类函数是完全可以拿来复用的。但是现在用了构造函数继承,由于它是复制了父类构造函数中的属性和方法,这样产生的每个子类实例中都会有一份自己各自的方法,可是有的方法完全没有必要复制,可以用来共用的,所以就说不能够「函数复用」。)

4.组合继承

既然原型链继承和构造继承都有这么多的缺点,那我们为何不阴阳结合,把它们组合在一起呢?

组合继承的概念:
组合继承就是将原型链继承与构造函数继承组合在一起,从而发挥两者之长的一种继承模式。

思路:

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

基操:

  • 通过call/apply在子类构造函数内部调用父类构造函数
  • 将子类构造函数的原型对象指向父类构造函数创建的一个匿名实例
  • 修正子类构造函数原型对象的constructor属性,将它指向子类构造函数

基操中第一点就是构造继承,第二点为原型链继承,第三点其实只是一个好的惯例,在后面的题目中会细讲到它。

4.1题目一
(理解组合继承的基本使用)

既然我都已经说了这么多关于组合继承的东西了,那想必你们也知道该如何设计一个组合继承了。
我现在需要你们来实现这么一个Child和Parent构造函数(代码尽可能地少),让它们代码的执行结果能如下:
(请先不要着急看答案哦,花上2分钟来思考一下,弄清每个属性在什么位置上,都有什么公共属性就好办了)

  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

解题思路:

  • 首先来看看俩构造函数产生的实例(child1和parent1)上都有name这个属性,所以name属性肯定是在父类的构造函数里定义的啦,而且是通过传递参数进去的。
  • 其次,sex属性只有实例child1才有,表明它是子类构造函数上的定义的属性(也就是我们之前提到过的公有属性)
  • 再然后child1和parent1都可以调用getName方法,并且都没有表现在实例上,所以它们可能是在Parent.prototype上。
  • 而getSex对于child1是可以调用的,对于father1是不可调用的,说明它是在Child.prototype上。

好的,下面看看如何实现它吧:

  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)
  console.log(parent1)
  child1.getName()
  child1.getSex()
  parent1.getName()
  parent1.getSex()

其实这是一道开放式题,如果构想的不一样也是正常了,不过你得自己把自己构想的用代码跑一边看看是不是和需求一样。
为什么说它比较开放呢?
就比如第一点,name属性,它不一定就只存在于Parent里呀,我Child里也可以有一个自己的name属性,只不过题目要求代码尽可能地少,所以最好的就是存在与Parent中,并且用.call来实现构造继承。
另外,getName方法也不一定要在Parent.prototype上,它只要存在于parent1的原型链中就可以了,所以也有可能在Object.prototype,脑补一下那张原型链的图,是不是这样呢?
这就是组合继承带来的魅力,如果你能看懂这道题,就已经掌握其精髓了。

4.2题目二
(理解constructor有什么作用)

拿上面👆那道题和最开始我们定义组合继承的基操做对比,发现第三点constructor好像并没有提到耶,但是也实现了我们想要的功能,那这样说来constructor好像并没有什么软用呀...
你想的没错,就算我们不对它进行任何的设置,它也丝毫不会影响到JS的内部属性。
它不过是给我们一个提示,用来标示实例对象是由哪个构造函数创建的。
先用一张图来看看constructor它存在的位置吧:

可以看到,它实际上就是原型对象的一个属性,指向的是构造函数。

所以我们是不是可以有这么一层关系:

  guaiguai.__proto__ = Cat.prototype
  Cat.prototype.constructor = Cat
  guaiguai.__proto__.constructor = Cat

再结合题目4.1来看,你觉得以下代码会打印出什么呢?题目其实还是4.1的题目,要求打印的东西不同而已:

  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)
  console.log(parent1.constructor)

答案:
f Parent () {}
f Parent () {}

打印出的两个都是parent函数。

parent1.constructor是Parent函数这个还好理解,结合上面的图片来看,只要通过原型链查找,我parent1实例自身没有constructor属性,那我就拿原型上的constructor,发现它指向得是构造函数Parent,因此第二个打印出Parent函数。

而对于child1,想想组合继承用到了原型链继承,虽然也用到了构造继承,但是构造继承对原型链之间的关系没有影响。那么我组合继承的原型链关系是不是就可以用原型链继承那张关系图来看?

如下:

就像上面看到的一样,原型链继承切断了原本child和Child原型对象的关系,而是重新指向了匿名实例。使得实例child1能够使用匿名实例原型链上的属性和方法。

当我们想要获取child1.constructor,肯定是向上查找,通过__proto__找它构造函数的原型对象匿名实例。

但是匿名实例它自身是没有constructor属性的,它只是Parent构造函数创建出来的一个对象而已,所以他也会继续向上查找,然后就找到了Parent原型对象上的constructor,也就是Parent了。

所以回过头来看看这句话:

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

从人(常)性(理)的角度上来看,child1是Child构建的,parent1是Parent构建的。

那么child1它的constructor就应该是Child呀,但是现在却变成了Parent,貌似并不太符合常理啊。

所以才有了这么一句:

  Child.prototype.constructor = Child

用以修复constructor的指向。

现在让我们通过改造原型链继承思维导图来画画组合继承的思维导图吧。

(至于为什么在组合继承中我修复了constructor,在原型链继承中没有,这个其实取决于你自己,因为你也看到了constructor实际并没有什么作用,不过面试被问到的话肯定是要知道的)

总结来说:

  • constructor它是构造函数原型对象中的一个属性,正常情况下它指向的是原型对象。
  • 它并不会影响任何JS内部属性,只是用来标示一下某个实例是由哪个构造函数产生的而已。
  • 如果我们使用了原型链继承或者组合继承无意间修改了constructor的指向,那么出于编程习惯,我们最好将它修改为正确的构造函数。

4.3题目三
(constructor的某个使用场景)

先来看看下面这道题:

  var a;
  (function () {
        function A() {
              this.a = 1
              this.b = 2
        }
        A.prototype.logA = function() {
              coonsole.log(this.a)
        }
        a = new A()
  })()
  a.logA()

结果:

  1

乍一看被整片的a给搞糊了,但是仔细分析来,就能得出结果了。

  • 定义了一个全局的变量a,和一个构造函数A
  • 在立即执行函数中,是可以访问到全局变量a的,因此a被赋值为了一个构造函数A生成的对象
  • 并且a对象中有两个属性:a和b,且值都是1
  • 之后在外层调用a.logA(),打印出的就是a.a,也就是1

难度升级:

现在我想要在匿名函数外给A这个构造函数的原型对象中添加一个方法logB用以打印出this.b。

你首先想到的是不是B.prototype.logB = funciton() {}。

但是注意咯,我是要你在匿名函数外添加,而此时由于作用域的原因,我们在匿名函数外是访问不到A的,所以这样的做法就不可行了。

解决办法:

虽然我们在外层访问不到A,但是我们可以通过原型链查找,来获取A的原型对象啊。
还是这张图:

这里我们就有两种解决办法了:

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

  a.__proto__.logB = function() {
        console.log(this.b)
  }
  a.logB()

2.通过a.constructor.prototype来访问到原型对象:

  a.constructor.prototype.logB = function() {
        console.log(this.b)
  }
  a.logB()

想想是不是这样的?

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

4.4题目四
(理解组合继承的优点)

  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)
  console.log(child2)
  console.log(Child.prototype)

  console.log(child1 instanceof Child)
  console.log(child2 instanceof Parent)

答案:

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

  true
  true

解析思路:

  • 两个child的sex和name都没啥问题,而colors可能会有些疑问,因为colors是通过构造继承于父类的,并且是复制出来的属性,所以改变child1.colors并不会影响child2.colors。(类似题目3.3)
  • 而Child.prototype,是使用new Parent生成的,并且生成的时候是没有传递参数进去的,因此name和colors都是undefined。而且题目中又将constructor给修正指向了Child。
  • 最后两个true,是因为child1可以沿着它的原型链查找到Child.prototype和Parent.prototype。

现在你就可以看出组合继承的优点了吧,它其实就是将两种继承方式的优点结合起来。

  • 可以继承父类实例属性和方法,也能够继承父类原型属性和方法
  • 弥补了原型链继承中引用属性共享的问题
  • 可传参,可复用

4.5题目五
(理解组合继承的缺点)

  function Parent(name) {
        console.log(name)
        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)

执行结果:

  undefined
  child1
  Child{ name: 'child1' }
  Parent{ name: undefined }

我们虽然只调用了一次new Child(), 但是在Parent中却打印了两次name。

  • 第一次是原型继承的的时候,new Parent()
  • 第二次是构造继承的时候,Parent.call()调用的

也就是说,在使用组合继承的时候,会凭空多调用一次父类构造函数。

另外,我们想要继承父类构造函数的属性和方法采用的是构造继承,也就是复制一份到子类实例对象中,而此时由于调用了new Parent(),所以Child.prototype中也会有一份一模一样的属性,就例如这里的name:undefined,可是我子类实例对象自己已经有了一份,所以我怎么也用不上Child.prototype上面的了,那你凭空多出来的属性不就占了内存浪费了吗?

因此我们可以看出组合继承的缺点:

  • 使用组合继承时,父类构造函数会被调用两次
  • 并且生成了两个实例,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存。

4.6题目六
(考察你是否理解实例对象上引用类型和原型对象上引用类型的区别)

  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)
  console.log(child2.colors)
  console.log(child1.features)
  console.log(child2.features)

结果:

  ['white', 'yellow']
  ['black']
  ['cute', 'sunshine']
  ['cute', 'sunshine']

解析:

  • colors属性虽然定义在Parent构造函数中,但是Child通过构造函数复制了其中的属性,所以它存在于各个实例当中,改变child1里的colors就不会影响其他地方了。

  • features是定义在父类构造函数对象中的,是比new Parent()还要更深一层的对象,在child实例还有Child.prototype(也就是new Parent()产生出了的匿名实例)上都没有features属性,因此它们只能去它们共有的Parent.prototype上面拿了,所以这时候它们就是共用了一个features,因此改变了child1.features就会改变child2.features了。

可是不对呀,你刚刚不是还说了:
组合继承弥补了原型链继承中引用属性共享的问题
就在题4.4中,都还热乎着呢?怎么这里的features还是没有被解决啊,它们还是共享了。

它确实是解决了原型链继承中引用属性共享的问题啊,你想想这里Child.prototype是谁?
是不是new Parent()产生的那个匿名实例?而这个匿名实例中的引用类型是不是colors?而colors是不是确实不是共享的?
那就对了呀,我已经帮你解决了原型(匿名实例)中引用属性共享的问题了呀。
至于features是Parent.prototype上的属性,相当于是爷爷那一级别的了,这我可没法子。

总结-组合继承

同样的,让我们对组合继承也来做个总结吧:

实现方式:

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

优点:

  • 可以继承父类实例属性和方法,也能够继承父类原型属性和方法
  • 弥补了原型继承中引用属性共享的问题
  • 可传参,可复用

缺点:

  • 使用组合继承时,父类构造函数会被调用两次
  • 并且生成了两个实例,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存。

constructor总结

  • constructor它是构造函数原型对象中的一个属性,正常情况下它指向的是原型对象的构造函数。
  • 它并不会影响任何JS内部属性,只是用来标示一下某个实例是由哪个构造函数产生的而已。
  • 如果我们使用了原型链继承或者组合继承无意间修改了constructor的指向,那么出于编程习惯,我们最好将它修改为正确的构造函数。

5.寄生组合继承

唔...寄生这个词听着有点可怕啊...
它比组合继承还要牛批一点。
刚刚我们提了组合继承的缺点无非就是:

父类构造函数会被调用两次
生成了两个实例,在父类实例上产生了无用废弃的属性

那么有没有一种方式让我们直接跳过父类实例上的属性,而让我直接就能继承父类原型链上的属性呢?
也就是说,我们需要一个干净的实例对象,来作为子类的原型。并且这个干净的实例对象还得能继承父类原型对象里的属性。

咦~说到干净的对象,我就想到了一个方法:Object.create()。

让我们先来回忆一波它的用法:

  Object.create(proto, propertiesObject)
  • 参数一,需要指定的原型对象
  • 参数二,可选参数,给新对象自身添加新属性以及描述器

在这里我们主要讲解一下第一个参数proto,它的作用就是能指定你要新建的这个对象它的原型对象是谁。

怎么说呢?

就好比,我们使用 var parent1 = new Parent()创建了一个对象parent1,那parent1.__proto__就是Parent.prototype。

使用var obj = new Object()创建了一个对象obj,那obj.__proto__就是Object.prototype。

而这个Object.create()厉害了,它现在能指定你新建对象的__proto__。

这不正是我们想要的吗?我们现在只想要一个干净并且能链接到父类原型链上的对象。

来看看题目一:

5.1题目一
(理解寄生组合继承的用法)

  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)
  child1.getName()

  console.log(child1.__proto__)
  console.log(Object.create(null))
  console.log(new Object())

可以看到,上面这道题就是一个标准的寄生组合继承,它与组合继承的区别仅仅是Child.prototype不同。

我们使用使用了Object.create(Parent.prototype)创建了一个空的对象,并且这个对象的__proto__属性是指向Parent.prototype的。

来看看寄生组合继承的思维导图:

可以看到,现在的Parent()已经和child1没有关系了,仅仅是用了Parent.call(this)来复制了一下Parent里的属性和方法。

因此这道题的答案为:

  child1{name: 'child1', sex: 'boy'}
  child1
  Parent{}
  {}
  {}

题目解析:

  • 使用寄生组合继承,child1不仅仅有自己的实例属性sex, 而且还复制了父类中的属性name
  • 寄生组合继承使得实例child1能通过原型链查找,使用到Parent.prototype上的方法,因此打印出child1。

最后的三个空对象,我们就需要展开来看看了:

  • child1.__proto__也就是Child.prototype,也就是Object.create(Parent.prototype),这个空对象它的__proto__指向的就是我们想要的父类的原型对象,所以child1就能使用Parent.prototype上的方法了。

  • 而通过Object.create(null)创建的对象呢,这可就空的不能再空了,因为我们创建它的时候传递的参数是null,也就是将它的__proto__属性设置为null,那它就相当于是没有原型链了,连Object.prototype上的方法它都不能用了(比如toString(),hasOwnProperty())

  • 再来看看new Object(),这个其实很好理解了,Object本身就是一个构造函数,就像Parent、Child这种,只不过它的原型对象使我们常用的Object.prototype。

5.2题目二
虽然寄生组合继承和组合继承非常像,不过我们还是来看一道题巩固巩固吧。

  function Parent(name) {
        this.name = name
        this.face = 'cry'
        this.colors = ['white', 'black']
  }      
  Parent.prototype.features = ['cure']
  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)
  console.log(child2)
  child1.getFeatures()
  child2.getFeatures()

执行结果:

  Child {name: "child1", face: "smile", colors: Array(3), sex: "boy"}
  Child {name: "child2", face: "smile", colors: Array(2), sex: "boy", features: ['sunshine']}
  ["cure"]
  ["sunshine"]

让我们看看解题思路:

  • name、face、sex三个属性都没有啥问题,要注意的只是features属性,后面写的会覆盖前面的(类似题目3.2)

  • colors属性是通过构造继承复制过来的,所以改变child1.colors对其他实例没有影响,这个说过很多次了。

  • 要注意的就是这里的features,在没有执行child2.features = ['sunshine']这段代码之前,child1和child2都是共用原型链上的features,但是执行了这段代码之后,就相当于是给child2对象新增了一个名为features属性,所以这时候child2取的就是它自身了。

总结-寄生组合继承
寄生组合继承算是ES6之前一种比较完美的继承方式吧。
它避免了组合继承中调用两次父类构造函数,初始化两次实例属性的缺点。

所以它拥有了上述继承方式的优点:

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

6.原型式继承

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

伪代码如下:
(后面会细讲)

  function object(obj) {
        function F() {};
        F.prototype = obj;
        F.prototype.constructor = F;
        return new F();
  }

开始以为是多神秘的东西,但后来真正了解它之后感觉用的应该不多吧。。。

先来看看题目一。

6.1题目一
在真正开始看原型式继承之前,先来看个我们比较熟悉的东西:

  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)
  console.log(huaihuai.colors)

执行结果:

  {}
  {}
  心
  ['white', 'black']

这里用到了我们之前提到过的Object.create()方法。

在这道题中,Object.create(cat)会创建一个__proto__属性为cat的空对象。

所以你可以看到guaiguai和huaihuai都是一只空猫,但是它们却能用猫cat的属性。

6.2题目二

不怕你笑话,上面说的这种方式就是原型式继承,只不过在ES5之前,还没有Object.create()方法,所以就会用开头介绍的那段伪代码来代替它。

将题目6.1改造一下,让我们自己来实现一个Object.create().

我们就将要实现的函数命名为create()。

想想Object.create()的作用:

  • 它接受的是一个对象
  • 返回的是一个对象
  • 新对象的原型链中必须能找到传进来的对象

所以就有了这么一个方法:

  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)

执行结果为:

效果是和Object.create()差不多(只不过我们自定义的create返回的对象是构造函数F创建的)。

这就有小伙伴要问了,既然是需要满足

  • 新对象的原型链中必须能找到传进来的对象

这个条件的话,我这样写也可以实现啊:

  function create(obj) {
        var newObj = {}
        newObj.__proto__ = obj
        return newObj
  }

请注意了,我们是要模拟Object.create()方法,如果你都能使用__proto__了,那为何不干脆使用Object.create()呢(它们是同一时期的产物)

总结-原型式继承
由于它使用的不太多,这里就不多说了,不过还是要总结一下:

实现方式:

该方法的原理是创建一个构造函数,构造函数的原型指向对象,然后调用new操作符创建实例,并返回这个实例,本质是一个浅拷贝。
在ES5之后可以直接使用Object.create()方法来实现,而在这之前就只能手动实现一个了。(如题目6.2)

优点:

  • 在不用创建构造函数的情况下,实现了原型链继承,代码量减少了一部分。

缺点:

  • 一些引用数据操作的时候会出问题,两个实例会共用继承实例的引用数据类
  • 谨慎定义方法,以免定义方法与继承对象原型的方法重名
  • 无法直接给父级构造函数使用参数

7.寄生式继承

其实这个寄生式继承也没啥东西的,它就是原型式继承的基础上再封装一层,来增强对象,之后将这个对象返回。

来看看伪代码你就知道了:

  function createAnother(original) {
        var clone = Object.create(original); // 通过调用Object.create()函数创建一个新对象
        clone.fn = function() {}; // 以某种方式来增强对象
        return clone; // 返回这个对象
  }

7.1题目一
(了解寄生式继承的使用方式)

例如我现在想要继承某个对象上的属性,同时又想在新创建的对象中新增上一些其它的属性。

来看下面这两只猫咪:

  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)
  console.log(guaiguai)
  console.log(huaihuai)

题目解析:

  • guaiguai是一只经过加工的小猫咪,所以它会卖萌,因此调用actingCute()会打印卖萌
  • 两只猫都是通过Object.create()进行过原型式继承cat对象的,所以是共享使用cat对象中的属性
  • guaiguai经过createAnother新增了自身的实例方法actingCute,所以会有这个方法
  • huaihuai是一只空猫,因为heart、colors都是原型对象cat上的属性

执行结果:

  我是一只猫咪
  心
  ['white', 'black']
  {actingCute: f}
  {}

总结-寄生式继承

实现方式:

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

优点:

  • 再不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分。

缺点:

  • 一些引用数据操作的时候会出问题,两个实例会公用继承实例的引用数据类。
  • 谨慎定义方法,以免定义方法与继承对象原型的方法重名
  • 无法直接给父级构造函数使用参数

8.混入方式继承多个对象

过五关斩六将,咱终于到了ES5中的要讲的最后一种继承方式了。

这个混入方式继承其实很好玩,之前我们一直都是以一个子类继承一个父类,而混入方式继承就是教我们如何一个子类继承多个父类的。

在这边,我们需要用到ES6中的方法Object.assign()。

它的作用就是可以可以把多个对象的属性和方法拷贝到目标对象中,若是存在同名属性的话,后面的会覆盖前面(当然,这种拷贝是一种浅拷贝)

来看看伪代码:

  function Child() {
        Parent.call(this)
        OtherParent.call(this)
  }
  Child.prototype = Object.create(Parent.prototype)
  Object.assign(Child.prototype, OtherParent.prototype)
  Child.prototype.constructor = Child

8.1题目一
(理解混入方式继承的使用)

既然您都看到这了,说明实力已经很强了,要不,咱们直接上个复杂点的题。

  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)

这里就是采用了混入方式继承,在题目中标出来的地方就是不同于寄生组合继承的方法。

现在child1不仅复制了Parent上的属性和方法,还复制了OtherParent上的。
而且它不仅可以使用Parent.prototype的属性和方法,还能使用OtherParent.prototype上的

结果:

  boy
  ['white']
  {name: 'child', sex: 'boy', colors: ['white']}

8.2题目二
(理解混入方式继承的原型链结构)

  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)
  console.log(Child.prototype.__proto__ === OtherParent.prototype)
  console.log(child1 instanceof Parent)
  console.log(child1 instanceof OtherParent)

这四个输出你感觉会是什么?
先看一下原型链:

可以看到,其实它与前面我们画的寄生组合继承思维导图就多了OtherParent的那部分东西。

  • Child内使用了call/apply来复制构造函数OtherParent上的属性和方法

  • Child.prototype使用Object.assign()浅拷贝OtherParent.prototype上的属性和方法

    true
    false
    true
    false
    

9.class中的继承

构造函数中主要的几种继承方式都已经介绍的差不多了,接下来就让我们看看ES6中class的继承吧。
在class中继承主要是依靠两个东西:

  • extends
  • super

而且对于该继承的效果和我们之前介绍过的寄生组合继承方式一样。

一起看看题目一。

9.1题目一
(理解class中的继承)

既然它的继承和寄生组合继承方式一样,那么让我们将题目5.1的题目改造一下,用class的继承方式来实现它。

  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)
  child1.getName()

  console.log(child1 instanceof Child)
  console.log(child1 instanceof Parent)

结果如下:

  Child{name: 'child1', sex: 'boy'}
  child1
  true
  true

我们再来写一下寄生组合继承的实现方式:

  function Parent(name) {
        this.name = name
  }
  Parent.prototype.getName = function() {
        console.log(this.name)
  }

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

  var child = new Child('child1')
  console.log(child)
  child.getName()

  console.log(child instanceof Child)
  console.log(child instanceof Parent)

结果如下:

  Child{name: 'child1', sex: 'boy'}
  child1
  true
  true

这样好像看不出个啥,没事,我们上图:

class继承:

寄生组合继承:

可以看到,class的继承方式完全满足于寄生组合继承。

9.2题目二
(理解extends的基本作用)

可以看到上面那道题,我们用到了两个关键的东西:extends和super。

extends从字面上来看还是很好理解的,对某个东西的延伸继承。
那如果我们单单只用extends不用super呢?

  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()

其实这里的执行结果和没有隐去之前一样。

执行结果:

那我们是不是可以认为:

  class Child extends Parent {}
  // 等同于
  class Child extends Parent {
        constructor(...args) {
              super(...args)
        }
  }

其实这一步很好理解,还记得我们之前就提到过,在class中如果没有定义constructor方法的话,这个方法是会被默认添加的,那么这里我们没有使用constructor,它其实已经被隐式的添加和调用了。

所以我们可以看出extends的作用:

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

9.3题目三
(理解super的基本作用)
通过上面的题目看来,constructor貌似是可有可无的角色。
那么super呢,它在class中扮演的是一个什么角色呢
还是上面的题目,但是这次不使用super,看看会有什么效果:

  class Parent {
        constructor() {
              this.name = 'parent'
        }
  }
  class Child extends Parent {
        constructor() {
              // super(name)
        }
  }
  var child1 = new Child()
  console.log(child1)
  child1.getName()

执行发现它报错了:

  Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
at new Child

大致意思就是你必须得在constructor中调用一下super函数。
这样说来,constructor和super是一对好基友啊。

super函数咱还是不能省啊,有点像给父级类中传递参数的感觉。
这样想其实算是猜对了一部分,这其实和ES6的继承机制有关。

  • 我们知道在ES5中继承,实质上是先创造子类的实例对象this,然后再将父类的属性和方法添加到this上(使用的是Parent.call(this))。
  • 而在ES6中却不是这样的,它实质是先创造父类的实例对象this(也就是使用super()),然后再用子类的构造函数去修改this。

通俗理解就是,子类必须得在constructor中调用super方法,否则新建实例就会报错,因为子类自己没有自己的this对象,而是继承父类的this对象,然后对其加工,如果不调用super的话子类就得不到this对象。

这道题介绍的是super的基本作用,下面来说说它的具体用法吧。

9.4题目四
(super当作函数调用时)
super其实有两种用法,一种是当作函数来调用,还有一种是当做对象来使用。
之前那道题就是将它当成函数来调用的,而且我们知道constructor中还必须得执行super()。

其实,当super被当做函数调用时,代表着父类的构造函数。

虽然它代表着父类的构造函数,但是返回的却是子类的实例,也就是说super内部的this指向的是Child。

让我们来看道题验证一下:
(new.target指向当前正在执行的那个函数,你可以理解为new后面的那个函数)

  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()

  var parent1 = new Parent()

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

这道题中,我在父类的constructor中打印出new.target.name。

并且用了一个叫做instance的变量来盛放super()的返回值。

而刚刚我们已经说了,super的调用代表着父类构造函数,那么这边我在调用new Child的时候,它里面也执行了父类的constructor函数,所以console.log(new.target.name)肯定被执行了两遍了(一遍是new Child,一遍是new Parent)

所以这里的执行结果为:

  Child
  Child{}
  true

  Parent

  Child{}
  Parent{}
  • new.target代表的是new后面的那个函数,那么new.target.name表示的是这个函数名,所以在执行new Child的时候,由于调用了super(),所以相当于执行了Parent中的构造函数,因此打印出了'Child'。

  • 另外,关于super()的返回值instance,刚刚已经说了它返回的是子类的实例,因此instance会打印出Child{};并且instance和子类constructor中的this相同,所以打印出true。

  • 而执行new Parent的时候,new.target.name打印出的就是Parent了。

  • 最后分别将child1和parent1打印出来。

通过这道题我们可以看出来:

  • super当成函数调用时,代表的是父类的构造函数,且返回的是子类的实例,也就是此时super内部的this指向子类。

  • 在子类的constructor中super()就相当于Parent.constructor.call(this)

9.5题目五

(super当成函数调用时的限制)
刚刚已经说明了super当成函数调用的时候就相当于是用call来改变了父类构造函数中的this指向,那么它的使用有什么限制呢?

  • 子类constructor中如果要使用this的话就必须放在super()之后
  • super当成函数调用时只能在子类的constructor中使用

来看看这里:

  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

这也就符合了刚刚说到的第一点:子类constructor中如果要使用this的话就必须放在super()之后。

这点其实很好理解,还记得super的作用吗?在constructor中必须要有super(),它就是用来产生实例this的,那么在调用它之前,肯定访问不到this。

至于第二点,super被当成函数来调用的话就必须得放到constructor中,在其他的地方使用它就是我们接下来要说的super当成对象使用的情况。

9.6题目六
(super当成对象来使用时)

super如果当成一个对象来调用的话,那也可能存在于class里的不同地方啊。

比如constructor、子类实例方法、子类构造方法,在这些地方分别指代的是什么呢?

我们只需要记住:

  • 在子类的普通函数中super对象指向父类的原型对象
  • 在子类的静态方法中super对象指向父类

依靠着这个原则,我们来做下面这道题:

  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.instanceFn()
  Child.staticFn()
  console.log(child1)

首先我们知道:

  • getName为父类原型对象上的方法
  • getSex为父类原型对象上的方法
  • getColors为父类的静态方法
  • instanceFn为子类原型对象上的方法
  • staticFn为子类的静态方法

题目分析:

  • 在使用new Child('child1')创建child1的时候,会执行子类constructor中的方法,因此会执行super.getName(),而依靠准则一,此时的constructor中的第二个super指向的是父类的原型对象,因此此时super.getName()会被成功调用,并打印出'child1'。(第一个super是当成函数来调用)
  • 当child1创建完之后,执行了child1.instanceFn(),这时候依据准则一,instanceFn函数中的super指向的还是父类的原型对象,因此super.getSex()也会被成功调用,并打印出'boy'。
  • staticFn属于子类的静态方法,所以需要使用Child.staticFn()来调用,且依据准则二,此时staticFn中的super指向的是父类,也就是Parent这个类,因此调用其静态方法getColors成立,打印出['white']。
  • 最后需要打印出child1,我们只需要知道哪些是child1的实例属性和方法就可以了,通过比较很容易就发现,child1中就只有一个name属性是通过调用super(name)从父级那里复制来的,其它方法都不能被child1"表现"出来,但是可以调用。

所以执行结果为:

  child1
  boy
  ['white']
  Child{ name: 'child1' }

9.7题目七
(super当成对象调用父类方法时this的指向)

在做刚刚那道题的时候,你们就对super.getName()的打印结果没啥疑问吗?

既然super.getName(),getName是被super调用的,而我却说此时的super指向的是父类原型对象,那么getName内打印出的应该是父类原型对象上的name,也就是undefined,怎么会打印出child1呢?

带着这个疑问我写下了这道题:

  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()
  console.log(child1)

现在父类原型对象和子类实例对象child1上都有sex属性,且不相同。

如果按照this指向来看,调用super.getSex()打印出的应该是Parent.prototype上的sex,‘boy’。

就像是这样调用一样:Parent.prototype.getSex()。

但是结果却是:

  girl
  Child{sex:'girl'}

其实,扯了一大堆就是想告诉你:

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

也就是说,转换为伪代码就是:

  super.getSex.call(this)
  // 即
  Parent.prototype.getSex.call(this)

而且super其实还有一个特性,就是你在使用它的时候,必须得显式的指定它是作为函数使用还是对象来使用,否则会报错的。

比如下面这样就不可以:

  class Child extends Parent {
        constructor() {
              super() //不报错
              super.getSex() // 不报错
              console.log(super) //报错
        }
  }

9.8题目八
(了解extends的继承目标)
extends后面接着的继承目标不一定要是个class。

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

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

来看看这一题:

  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)
  console.log(child2)
  console.log(child3)

执行结果:

  Child1 {name: 'parent' }
  Child2{}
  Child3[1]
  • 可以继承构造函数Parent
  • 不存在任何继承,就是一个普通函数,所以直接继承Function.prototype
  • 可以继承原生构造函数

总结-class继承

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。
posted @ 2020-12-14 22:36  Yang-0394  阅读(78)  评论(0)    收藏  举报