你盼世界,我盼望你无bug
。Hello 你们好!我是霖呆呆!javascript
怎么样?小伙伴们,上一章《封装篇(牛刀小试)》里的十几道题是否是作着不过瘾啊。html
心里活动:就这点水平的东西?还号称魔鬼题?前端
能够,小伙子(姑娘),很膨胀,我喜欢。哈哈哈哈。java
既然这样的话,就来看看这系列的大头——继承?es6
这篇文章的继承题但是有点东西的啊,基本覆盖了全部主流的继承状况,并且都比较细节,若是你原来只是浅浅的看了一些教材,跟着手写实现了一下而已的话,那你看完保证是会有收获的!那样的话还请给个三连哦 😊。面试
☑️点赞➕收藏➕关注编程
❌ 闪现➕大招➕引燃segmentfault
老规矩,不然在评论区给我一个臭臭的👎。全文共有1.7w
字,前先后后整理了快两个星期(整理真的很容易掉头发😂)。数组
因此还请你找个安静的地方,在一个合适的时间来细细品味它 😊。bash
OK👌,废话很少说,咱走着,卡加(韩语)~
经过阅读本篇文章你能够学习到:
(在正式阅读本篇文章以前还请先查看封装篇,也就是目录的第一章节,以后观看温馨感更高哦 😁)
好滴👌,仍是让咱们先来了解一下继承的概念哈。
继承 🤔️?
"嗯...我爸在深圳福田有一套房,之后要继承给我"
"啪!"
"我提莫的在想什么?我还有个弟弟,因此我爸得有两套"
"啪!"
"你提莫还在睡,该搬砖了!"
正经点的,其实一句话来讲:
继承就是子类可使用父类的全部功能,而且对这些功能进行扩展。
好比我有个构造函数A
,而后又有个构造函数B
,可是B
想要使用A
里的一些属性和方法,一种办法就是让咱们自身化身为CV侠
,复制粘贴一波。还有一种就是利用继承,我让B
直接继承了A
里的功能,这样我就能用它了。
今天要介绍的八种继承方式在目录中都已经列举出来了。
不着急,从浅到深咱一个个来看。
将子类的原型对象指向父类的实例
(理解原型链继承的概念)
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
上。这看着不就是以前都讲到过的内容嘛?
就像是题目1.6
和1.7
同样(《封装篇(牛刀小试)》里的)。
因此如今你知道了吧,这种方式就叫作原型链继承。
将子类的原型对象指向父类的实例。
咱们来写个伪代码,方便记忆:
Child.prototype = new Parent() 复制代码
固然,更加严谨一点的作法其实还有一步:Child.prototype.constructor = Child
,不过这边霖呆呆先卖个关子,到题目4.2
中咱们再来详细说它。
不知道大家在看到原型链继承这个词语的时候,第一时间想到的是什么?
有没有和我同样,想到的是把子类的原型对象指向父类的原型对象的😂:
Child.prototype = Parent.prototype
复制代码
和我同样的举个手给我看下🙋♂️,😂
以后我就为我xx似的想法感到惭愧...
若是我只能拿到父类原型链上的属性和方法那也太废了吧,我可不止这样,我还想拿到父类构造函数上的属性。
因此这道题:
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()
这种方式。
(理解原型链继承的优势和缺点)
这道题的结果你们能想到吗?
请注意对象是地址引用的哦。
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
和feature
都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
和colors
属于原型对象上的属性并不会被表现出来)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
能够看出来)这...这看到没,压根就不须要记,想一想霖呆呆出的这道变态的题面试的时候被问到脱口就来了。
这道题主要是想介绍一个重要的运算符: instanceof
先看看官方的简介:
instanceof
运算符用于检测构造函数的 prototype
属性是否出如今某个实例对象的原型链上。
再来看看通俗点的简介:
a instanceof B
实例对象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
它会向它的原型链中不停的查找,看有没有右边那个构造函数的原型对象。
例如child1 instanceof Child
的查找顺序:
child1 -> child1.__proto__ -> Child.prototype
复制代码
child1 instanceof Parent
的查找顺序:
child1 -> child1.__proto__ -> Child.prototype
-> Child.prototype.__proto__ -> Parent.prototype
复制代码
还不理解?
不要紧,我还有大招:
我在上面👆原型链继承的思惟导图上加了三个查找路线。
被⭕️标记的一、二、3
分别表明的是Child、Parent、Object
的原型对象。
好滴,一张图简洁明了。之后再碰到instanceof
这种东西,按照我图上的查找路线来查找就能够了 😁 ~
(若是你能看到这里,你就会发现霖呆呆的美术功底,不是通常的强)
[表情包害羞~]
(了解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 复制代码
判断的方式只要把原型链继承instanceof查找思惟导图这张图反过来查找便可。
了解了最简单的原型链继承,再让咱们来看看构造继承呀,也叫作构造函数继承。
在子类构造函数内部使用call或apply
来调用父类构造函数
为了方便你查看,咱们先来复习一波.call
和apply
方法。
经过call()、apply()
或者bind()
方法直接指定this
的绑定对象, 如foo.call(obj)
使用.call()
或者.apply()
的函数是会直接执行的
而bind()
是建立一个新的函数,须要手动调用才会执行
.call()
和.apply()
用法基本相似,不过call
接收若干个参数,而apply
接收的是一个数组
(构造继承的基本原理)
因此来看看这道题?
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
表示的是你能够往里面传递参数,固然这只是伪代码)
若是你以为上面👆这道题还不具备说明性,咱们来看看这里。
如今我在子类和父类中都加上name
这个属性,你以为生出来的会是好孩子仍是坏孩子呢?
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) 复制代码
实际上是好是坏很好区分,只要想一想3.1
里,把Parent.call(this, 'good boy')
换成伪代码就知道了。
换成了伪代码以后,等因而重复定义了两个相同名称的属性,固然是后面的覆盖前面的啦。
因此结果为:
Child {sex: "boy", name: "bad boy"} 复制代码
这道题若是换一下位置:
function Child () { this.sex = 'boy' this.name = 'bad boy' Parent.call(this, 'good boy') } 复制代码
这时候就是好孩子了。
(哎,霖呆呆的产生可能就是第二种状况...)
(构造继承的优势)
解决了原型链继承中子类共享父类引用对象的问题
刚刚的题目都是一些基本数据类型,让我来加上引用类型看看
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
并不会影响到其它的实例(child2
)耶。
这里的缘由其实咱们前面也说了:
使用父类的构造函数来加强子类实例,等因而复制父类的实例属性给子类。
因此如今child1
和child2
如今分别有它们各自的colors
了,就不共享了。
并且这种拷贝属于深拷贝,验证的方式是你能够把colors
数组中的每一项改成一个对象,而后修改它看看。
function Parent () { //... this.colors = [{ title: 'white' }, { title: 'black' }] } 复制代码
所以咱们能够得出构造继承的优势:
(构造继承的缺点一)
在了解继承的时候,咱们老是会想到原型链上的属性和方法能不能被继承到。
采用了这种构造继承的方式,能不能继承父类原型链上的属性呢?
来看下面👇这道题目
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 复制代码
sex、name
属性都有这个咱们均可以理解getSex
属于Child
构造函数原型对象上的方法,咱们确定是能用它的,这个也好理解getName
呢?它属于父类构造函数原型对象上的方法,报错了?怎么滴?我子类不配使用你啊?你确实是不配使用我。
你使用Parent.call(this, 'good boy')
只不过是让你复制了一下我构造函数里的属性和方法,可没说能让你复制我原型对象的啊~年轻人,不要这么贪嘛。
因此咱们能够看出构造继承一个最大的缺点,那就是:
小气!
"啪!"
"你给我正经点"
😂
实际上是:
"那不就是小气嘛..."
"..."
(构造继承的缺点二)
它的第二个缺点是:实例并非父类的实例,只是子类的实例。
停一下,让咱们先来思考一下这句话的意思,而后想一想怎样来验证它呢 🤔️ ?
一分钟...二分钟...三分钟...
啊,我知道了,刚刚不是才学的一个叫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
检测到你和父类有关系的话,那就会对这些凭空产生的属性和方法有所质疑...)
所以构造继承第二个缺点是:
构造继承总结来讲:
优势:
3.3
)缺点:
3.4
)3.5
)(最后一个缺点‘没法实现函数复用’通过评论区小伙伴matteokjh的提醒,我理解的大概是这个意思:父类构造函数中的某个函数可能只是一个功能型的函数,它不论被复制了多少份,输出的结果或者功能都是同样的,那么这类函数是彻底能够拿来复用的。可是如今用了构造函数继承,因为它是复制了父类构造函数中的属性和方法,这样产生的每一个子类实例中都会有一份本身各自的方法,但是有的方法彻底没有必要复制,能够用来共用的,因此就说不可以「函数复用」。)
既然原型链继承和构造继承都有这么多的缺点,那咱们为什么不阴阳结合,把它们组合在一块儿呢?
咦~
好像是个好想法。
把咱们前面的伪代码拿来用用,想一想该如何组合呢?
// 原型链继承 Child.prototype = new Parent() // 构造继承 function Child () { Parent.call(this, ...arguments) } 复制代码
...思考中🤔...
看到这两段伪代码,我好像有所顿悟了,不就是按照伪代码里写的,把这两种继承组合在一块儿吗?
哇!这都被我猜中了,搜索一下组合继承的概念,果真就是这样。
组合继承的概念:
组合继承就是将原型链继承与构造函数继承组合在一块儿,从而发挥二者之长的一种继承模式。
思路:
基操:
call/apply
在子类构造函数内部调用父类构造函数constructor
属性,将它指向子类构造函数基操中的第一点就是构造继承,第二点为原型链继承,第三点其实只是一个好的惯例,在后面的题目会细讲到它。
(理解组合继承的基本使用)
如今我决定对大家再也不仁慈,让咱们换种想法,逆向思惟来解解题好很差。
阴笑~
既然我都已经说了这么多关于组合继承的东西了,那想必大家也知道该如何设计一个组合继承了。
我如今须要大家来实现这么一个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
,脑补一下那张原型链的图,是否是这样呢?
这就是组合继承带来的魅力,若是你能看懂这道题,就已经掌握其精髓了 👏。
(理解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
了。
因此回过头来看看这句话:
construcotr它不过是给咱们一个提示,用来标示实例对象是由哪一个构造函数建立的。
从人(常)性(理)的角度上来看,child1
是Child
构建的,parent1
是Parent
构建的。
那么child1
它的constructor
就应该是Child
呀,可是如今却变成了Parent
,貌似并不太符合常理啊。
因此才有了这么一句:
Child.prototype.constructor = Child
复制代码
用以修复constructor
的指向。
如今让咱们经过改造原型链继承思惟导图
来画画组合继承的思惟导图
吧。
(至于为何在组合继承中我修复了constructor
,在原型链继承中没有,这个其实取决于你本身,由于你也看到了constructor
实际并无什么做用,不过面试被问到的话确定是要知道的)
总结来讲:
constructor
它是构造函数原型对象中的一个属性,正常状况下它指向的是原型对象。JS
内部属性,只是用来标示一下某个实例是由哪一个构造函数产生的而已。constructor
的指向,那么出于编程习惯,咱们最好将它修改成正确的构造函数。(constructor
的某个使用场景)
先来看看下面👇这道题:
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
给搞糊了,可是仔细分析来,就能得出结果了。
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
的原型对象呀。
仍是这张图:
这里咱们就有两种解决办法了:
a.__proto__
来访问到原型对象:a.__proto__.logB = function () { console.log(this.b) } a.logB() 复制代码
a.constructor.prototype
来访问到原型对象:a.constructor.prototype.logB = function () { console.log(this.b) } a.logB() 复制代码
想一想是否是这样的?
虽然我a
实例上没有constructor
,可是原型对象上有呀,因此a.construtor
实际拿的是原型对象上的construtor
。
(我的愚见感受并没什么软用...我用__proto__
就能够了呀 😂)
(理解组合继承的优势)
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(child1 instanceof Parent) 复制代码
有了前面几题做为基础,这道题也就不难了。
答案:
Child{ sex: "boy", name: "child1", colors: ["white", "yellow"] } Child{ sex: "boy", name: "child2", colors: ["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
。(相似题目2.1
)如今你就能够看出组合继承的优势了吧,它其实就是将两种继承方式的优势给结合起来。
(理解组合继承的缺点)
人无完人,狗无完狗,就算是组合继承这么牛批的继承方式也仍是有它的缺点 😁。
一块儿来看看这里:
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) 复制代码
执行结果为:
undefined 'child1' Child{ name: 'child1' } Parent{ name: undefined } 复制代码
咱们虽然只调用了new Child()
一次,可是在Parent
中却两次打印出了name
。
new Parent()
Parent.call()
调用的也就是说,在使用组合继承的时候,会凭空多调用一次父类构造函数。
另外,咱们想要继承父类构造函数里的属性和方法采用的是构造继承,也就是复制一份到子类实例对象中,而此时因为调用了new Parent()
,因此Child.prototype
中也会有一份如出一辙的属性,就例如这里的name: undefined
,但是我子类实例对象本身已经有了一份了呀,因此我怎么也用不上Child.prototype
上面的了,那你这凭空多出来的属性不就占了内存浪费了吗?
所以咱们能够看出组合继承的缺点:
(考察你是否理解实例对象上引用类型和原型对象上引用类型的区别)
这里可就有一个坑了,得注意了⚠️:
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) 复制代码
题目解析:
colors
属性虽然定义在Parent
构造函数中,可是Child
经过构造继承复制了其中的属性,因此它存在于各个实例当中,改变child1
里的colors
就不会影响其它地方了features
是定义在父类构造函数原型对象中的,是比new Parent()
还要更深一层的对象,在child
实例还有Child.prototype
(也就是new Parent()
产生出了的匿名实例)上都没有features
属性,所以它们只能去它们共有的Parent.prototype
上面拿了,因此这时候它们就是共用了一个features
,所以改变child1.features
就会改变child2.features
了。结果为:
["white", "yellow"] ["black"] ["cute", "sunshine"] ["cute", "sunshine"] 复制代码
但是霖呆呆不对呀,你刚刚不是还说了:
组合继承弥补了原型链继承中引用属性共享的问题
就在题4.4
中,都还热乎着呢?怎么这里的features
仍是没有被解决啊,它们仍是共享了。
"冤枉啊!我历来不骗人"
它确实是解决了原型链继承中引用属性共享的问题啊,你想一想这里Child.prototype
是谁?
是否是new Parent()
产生的那个匿名实例?而这个匿名实例中的引用类型是否是colors
?而colors
是否是确实不是共享的?
那就对了呀,我已经帮你解决了原型(匿名实例
)中引用属性共享的问题了呀。
至于features
是Parent.prototype
上的属性,至关因而爷爷那一级别的了,这我可无法子。
一样的,让咱们对组合继承也来作个总结吧:
实现方式:
优势:
缺点:
constructor总结:
constructor
它是构造函数原型对象中的一个属性,正常状况下它指向的是原型对象。JS
内部属性,只是用来标示一下某个实例是由哪一个构造函数产生的而已。constructor
的指向,那么出于编程习惯,咱们最好将它修改成正确的构造函数。唔...寄生这个词听着有点可怕啊...
它比组合继承还要牛批一点。
刚刚咱们提了组合继承的缺点无非就是:
那么有没有一种方式让咱们直接跳过父类实例上的属性,而让我直接就能继承父类原型链上的属性呢?
也就是说,咱们须要一个干净的实例对象,来做为子类的原型。而且这个干净的实例对象还得能继承父类原型对象里的属性。
咦~说到干净的对象,我就想到了一个方法: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__
。
哈哈哈哈~
这正不是咱们想要的吗?咱们如今只想要一个干净而且能连接到父类原型链上的对象。
来看看题目一。
(理解寄生组合继承的用法)
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
里的属性和方法 😁。
所以这道题的答案为:
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{ sex: "boy", name: "child1" } child1.getName() // "child1" console.log(child1.__proto__) // Parent{} console.log(Object.create(null)) // {} console.log(new Object()) // {} 复制代码
题目解析:
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
。(看看,你们在学继承的同时,还顺便学习了一波Object.create()
,多好啊 😁)
虽然寄生组合继承和组合继承很是像,不过咱们仍是来看一道题巩固巩固吧。
执行结果:
Child{ name: 'child1', face: 'smile', sex: 'boy', colors: ['white', 'black', 'yellow'] } Child{ name: 'child2', face: 'smile', sex: 'boy', colors: ['white', 'black'], features: ['sunshine'] } ["cute"] ["sunshine"] 复制代码
哈哈哈,小伙伴们的答案和这里是否有出入呢?
是否是发现一不当心就会作错 😂。
让咱们来看看解题思路:
name、face、sex
三个属性都没有啥问题,要注意的只是face
属性,后面写的会覆盖前面的(相似题目3.2
)colors
属性是经过构造继承复制过来的,因此改变child1.colors
对其余实例没有影响,这个说过不少次了。features
,在没有执行child2.features = ['sunshine']
这段代码以前,child1
和child2
都是共用原型链上的features
,可是执行了这段代码以后,就至关因而给child2
对象上新增了一个名为features
属性,因此这时候child2
取的就是它自身的了。(这道题我是使用VSCode
插件Polacode-2019
作的代码截图,不知道你们是喜欢这种代码截图仍是喜欢源代码的形式呢?能够留言告诉霖呆呆 😁)
(另外,关于更多美化工具的使用能够查看个人这篇文章:你的掘金文章本能够这么炫(博客美化工具一波带走))
寄生组合继承算是ES6
以前一种比较完美的继承方式吧。
它避免了组合继承中调用两次父类构造函数,初始化两次实例属性的缺点。
因此它拥有了上述全部继承方式的优势:
instanceOf
和isPrototypeOf
方法算是翻了不少关于JS
继承的文章吧,其中百分之九十都是这样介绍原型式继承的:
该方法的原理是建立一个构造函数,构造函数的原型指向对象,而后调用 new 操做符建立实例,并返回这个实例,本质是一个浅拷贝。
伪代码以下:
(后面会细讲)
function objcet (obj) { function F () {}; F.prototype = obj; F.prototype.constructor = F; return new F(); } 复制代码
开始觉得是多神秘的东西,但后来真正了解了它以后感受用的应该很少吧... 😢
先来看看题目一。
在真正开始看原型式继承以前,先来看个咱们比较熟悉的东西:
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
的空对象。
因此你能够看到乖乖
和坏坏
都是一只空猫,可是它们却能用猫cat
的属性。
不怕你笑话,上面👆说的这种方式就是原型式继承,只不过在ES5
以前,尚未Object.create()
方法,因此就会用开头介绍的那段伪代码来代替它。
将题目6.1
改造一下,让咱们本身来实现一个Object.create()
。
咱们就将要实现的函数命名为create()
。
想一想Object.create()
的做用:
因此就有了这么一个方法:
function objcet (obj) { function F () {}; F.prototype = obj; F.prototype.constructor = F; return new F(); } 复制代码
它知足了上述的几个条件。
来看看效果是否是和题6.1
同样呢?
function objcet (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
)。
优势:
缺点:
(呀!很久没用表情包了,此处应该有个表情包)
cccc...
怎么又来了个什么寄生式继承啊,还有完没完...
心态放平和...
其实这个寄生式继承也没啥东西的,它就是在原型式继承的基础上再封装一层,来加强对象,以后将这个对象返回。
来看看伪代码你就知道了:
function createAnother (original) { var clone = Object.create(original);; // 经过调用 Object.create() 函数建立一个新对象 clone.fn = function () {}; // 以某种方式来加强对象 return clone; // 返回这个对象 } 复制代码
(了解寄生式继承的使用方式)
它的使用方式,唔...
例如我如今想要继承某个对象上的属性,同时又想在新建立的对象中新增上一些其它的属性。
来看下面👇这两只猫咪
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: ƒ } {} 复制代码
实现方式:
优势:
缺点:
过五关斩六将,咱终于到了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 复制代码
(理解混入方式继承的使用)
额,既然您都看到这了,说明实力以及很强了,要不?咱直接就上个复杂点的题?
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'] } 复制代码
(理解混入方式继承的原型链结构)
同是上面👆的题,我如今多加上几个输出:
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) 复制代码
这四个输出你感受会是什么 🤔️?
先不要着急,若是有条件的,本身动手在纸上把如今的原型链关系给画一下。
反正呆呆是已经用XMind
的画好了:
能够看到,其实它与前面咱们画的寄生组合继承思惟导图就多了下面OtherParent
的那部分东西。
Child
内使用了call/apply
来复制构造函数OtherParent
上的属性和方法Child.prototype
使用Object.assign()
浅拷贝OtherParent.prototype
上的属性和方法根据这这幅图,咱们很快就能得出答案了:
true false true false 复制代码
构造函数中主要的几种继承方式都已经介绍的差很少了,接下来就让咱们看看ES6
中class
的继承吧。
在class
中继承主要是依靠两个东西:
extends
super
并且对于该继承的效果和以前咱们介绍过的寄生组合继承方式同样。(没错,就是那个最屌的继承方式)
一块儿来看看题目一 😁。
(理解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) { 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) child1.getName() console.log(child1 instanceof Child) console.log(child1 instanceof Parent) 复制代码
结果以下:
Child{ name: 'child1', sex: 'boy' } 'child1' true true 复制代码
这样好像看不出个啥,没事,让咱们上图:
class
继承:
寄生组合继承:
能够看到,class
的继承方式彻底知足于寄生组合继承。
(理解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) } } 复制代码
OK👌,其实这一步很好理解啦,还记得以前咱们就提到过,在class
中若是没有定义constructor
方法的话,这个方法是会被默认添加的,那么这里咱们没有使用constructor
,它其实已经被隐式的添加和调用了。
因此咱们能够看出extends
的做用:
class
能够经过extends
关键字实现继承父类的全部属性和方法extends
实现继承的子类内部没有constructor
方法,则会被默认添加constructor
和super
。(理解super
的基本做用)
经过上面那道题看来,constructor
貌似是无关紧要的角色。
那么super
呢,它在 class
中扮演的是一个什么角色 🤔️?
仍是上面的题目,可是此次我不使用super
,看看会有什么效果:
class Parent { constructor () { this.name = 'parent' } } class Child extends Parent { constructor () { // super(name) // 把super隐去 } } 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
的基本做用,下面来讲说它的具体用法吧。
(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
和子类construtor
中的this
相同,因此打印出true
。new Parent
的时候,new.target.name
打印出的就是'Parent'
了。child1
和parent1
打印出来,都没什么问题。经过这道题咱们能够看出:
super
当成函数调用时,表明父类的构造函数,且返回的是子类的实例,也就是此时super
内部的this
指向子类。constructor
中super()
就至关因而Parent.constructor.call(this)
(super
当成函数调用时的限制)
刚刚已经说明了super
当成函数调用的时候就至关因而用call
来改变了父类构造函数中的this
指向,那么它的使用有什么限制呢?
constructor
中若是要使用this
的话就必须放到super()
以后super
当成函数调用时只能在子类的construtor
中使用来看看这里:
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) 复制代码
你以为这里会打印出什么呢 🤔️?
其实这里啥都不会打印,控制台是红色的。
报了个和7.3
同样的错:
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
的啦。
也就是在this.sex = 'boy'
这一步的时候就已经报错了。
至于第二点,super
被当成函数来调用的话就必须得放到constructor
中,在其它的地方使用它就是咱们接下来要说的super
当成对象使用的状况。
(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) 复制代码
经过学习《【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)》咱们知道各个方法所在的位置:
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' } 复制代码
"Good for you! 我貌似已经掌握它嘞"
(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()
转换为伪代码就是:
super.getSex.call(this) // 即 Parent.prototype.getSex.call(this) 复制代码
(别看这里扯的多,可是多看点例子🌰的话理解必定会加深入的)
并且super
其实还有一个特性,就是你在使用它的时候,必须得显式的指定它是做为函数使用仍是对象来使用,不然会报错的。
好比下面这样就不能够:
class Child extends Parent { constructor () { super() // 不报错 super.getSex() // 不报错 console.log(super) // 这里会报错 } } 复制代码
(了解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
继承咋有这么多讲的啊。
不过总算是我也说完,你也看完了...
OK👌,来个总结呗。
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
。唔...写到最后我感受仍是要将全部的继承状况来作一个总结,这边只总结出实现方式的伪代码以及原型链思惟导图,具体的优缺点在各个模块中已经总结好了就不重复了。
伪代码:
Child.prototype = new Parent() 复制代码
思惟导图:
伪代码:
function Child () { Parent.call(this, ...arguments) } 复制代码
伪代码:
// 构造继承 function Child () { Parent.call(this, ...arguments) } // 原型链继承 Child.prototype = new Parent() // 修正constructor Child.prototype.constructor = Child 复制代码
思惟导图:
伪代码:
// 构造继承 function Child () { Parent.call(this, ...arguments) } // 原型式继承 Child.prototype = Object.create(Parent.prototype) // 修正constructor Child.prototype.constructor = Child 复制代码
思惟导图:
伪代码:
var child = Object.create(parent) 复制代码
伪代码:
function createAnother (original) { var clone = Object.create(original);; // 经过调用 Object.create() 函数建立一个新对象 clone.fn = function () {}; // 以某种方式来加强对象 return clone; // 返回这个对象 } 复制代码
伪代码:
function Child () { Parent.call(this) OtherParent.call(this) } Child.prototype = Object.create(Parent.prototype) Object.assign(Child.prototype, OtherParent.prototype) Child.prototype.constructor = Child 复制代码
思惟导图:
伪代码:
class Child extends Parent { constructor (...args) { super(...args) } } 复制代码
知识无价,支持原创。
参考文章:
你盼世界,我盼望你无bug
。这篇文章就介绍到这里。
其实实现继承的方式真的有好多种啊~
我在写以前还考虑要不要把这些状况都写进去,由于那样题目势必会不少。
可是后来我反思了一下本身
"啪!"
"我提莫在想什么?"
霖呆呆我出这些题不就是为了难为你嘛,那我还在顾虑什么~
另外细心的小伙伴数了数总题数,这也就只有31
道啊,哪来的48
道题。
(我把《封装篇》里的那17
道也算进来了,怎么滴...你又不是不知道霖呆呆我是标题党)
如今将题目所有弄懂以后是否是对面向对象以及原型链更加熟悉了呢 😁。
没点赞的小伙伴还请给波赞哦👍,你的每一个赞对我都很重要 😊。
喜欢霖呆呆的小伙还但愿能够关注霖呆呆的公众号 LinDaiDai
或者扫一扫下面的二维码👇👇👇.
我会不定时的更新一些前端方面的知识内容以及本身的原创文章🎉
你的鼓励就是我持续创做的主要动力 😊.
相关推荐:
《【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理)》