你盼世界,我盼望你无bug
。Hello 大家好!我是霖呆呆!
怎么样?小伙伴们,上一章《封装篇(牛刀小试)》里的十几道题是不是做着不过瘾啊。
内心活动:就这点水平的东西?还号称魔鬼题?
可以,小伙子(姑娘),很膨胀,我喜欢。
哈哈哈哈。
既然这样的话,就来看看这系列的大头——继承?
这篇文章的继承题可是有点东西的啊,基本覆盖了所有主流的继承情况,而且都比较细节,如果你原来只是浅浅的看了一些教材,跟着手写实现了一下而已的话,那你看完保证是会有收获的!那样的话还请给个三连哦 😊。
☑️点赞➕收藏➕关注
❌ 闪现➕大招➕引燃
老规矩,否则在评论区给我一个臭臭的👎。
全文共有1.7w
字,前前后后整理了快两个星期(整理真的很容易掉头发😂)。
所以还请你找个安静的地方,在一个合适的时间来细细品味它 😊。
OK👌,废话不多说,咱走着,卡加(韩语)~
通过阅读本篇文章你可以学习到:
- 封装
ES6
之前的封装-构造函数ES6
之后的封装-class
- 继承(本篇)
- 原型链继承
- 构造继承
- 组合继承
- 寄生组合继承
- 原型式继承
- 寄生继承
- 混入式继承
class
中的extends
继承
- 多态
(在正式阅读本篇文章之前还请先查看封装篇,也就是目录的第一章节,之后观看舒适感更高哦 😁)
好滴👌,还是让我们先来了解一下继承的概念哈。
继承 🤔️?
"嗯...我爸在深圳福田有一套房,以后要继承给我"
"啪!"
"我提莫的在想什么?我还有个弟弟,所以我爸得有两套"
"啪!"
"你提莫还在睡,该搬砖了!"
正经点的,其实一句话来说:
继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展。
比如我有个构造函数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
还不理解?
没关系,我还有大招:
我在上面👆原型链继承的思维导图上加了三个查找路线。
被⭕️标记的1、2、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)
}
}
知识无价,支持原创。
参考文章:
- 《MDN-Object.prototype.hasOwnProperty()》
- 《es6中class类的全方面理解(一)》
- 《瓶子君-JS 继承的 六 种实现方式》
- 《[js各种方法继承以及优缺点]》
- 《JS菌-💎 一文看懂 JS 继承》
- 《zhangfaliang-史上最为详细的javascript继承》
- 《阮一峰的ES6---Class的继承》
你盼世界,我盼望你无bug
。这篇文章就介绍到这里。
其实实现继承的方式真的有好多种啊~
我在写之前还考虑要不要把这些情况都写进去,因为那样题目势必会很多。
但是后来我反思了一下自己
"啪!"
"我提莫在想什么?"
霖呆呆我出这些题不就是为了难为你嘛,那我还在顾虑什么~
另外细心的小伙伴数了数总题数,这也就只有31
道啊,哪来的48
道题。
(我把《封装篇》里的那17
道也算进来了,怎么滴...你又不是不知道霖呆呆我是标题党)
现在将题目全部弄懂之后是不是对面向对象以及原型链更加熟悉了呢 😁。
没点赞的小伙伴还请给波赞哦👍,你的每个赞对我都很重要 😊。
喜欢霖呆呆的小伙还希望可以关注霖呆呆的公众号 LinDaiDai
或者扫一扫下面的二维码👇👇👇.
我会不定时的更新一些前端方面的知识内容以及自己的原创文章🎉
你的鼓励就是我持续创作的主要动力 😊.
相关推荐:
《【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理)》