何不三连】做完这48道题彻底弄懂JS继承(1.7w字含辛整理-返璞归真)

JavaScript对象封装、多态、继承
前言
你盼世界,我盼望你无bug。Hello 大家好!我是霖呆呆!
怎么样?小伙伴们,上一章《封装篇(牛刀小试)》里的十几道题是不是做着不过瘾啊。
内心活动:就这点水平的东西?还号称魔鬼题?
可以,小伙子(姑娘),很膨胀,我喜欢。
哈哈哈哈。
既然这样的话,就来看看这系列的大头——继承?
这篇文章的继承题可是有点东西的啊,基本覆盖了所有主流的继承情况,而且都比较细节,如果你原来只是浅浅的看了一些教材,跟着手写实现了一下而已的话,那你看完保证是会有收获的!那样的话还请给个三连哦 😊。
☑️点赞➕收藏➕关注
❌ 闪现➕大招➕引燃
老规矩,否则在评论区给我一个臭臭的👎。
全文共有1.7w字,前前后后整理了快两个星期(整理真的很容易掉头发😂)。
所以还请你找个安静的地方,在一个合适的时间来细细品味它 😊。
OK👌,废话不多说,咱走着,卡加(韩语)~
JS继承系列介绍
通过阅读本篇文章你可以学习到:

封装

ES6之前的封装-构造函数
ES6之后的封装-class

继承(本篇)

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

多态

(在正式阅读本篇文章之前还请先查看封装篇,也就是目录的第一章节,之后观看舒适感更高哦 😁)
继承
好滴👌,还是让我们先来了解一下继承的概念哈。
继承 🤔️?
“嗯…我爸在深圳福田有一套房,以后要继承给我”
“啪!”
“我提莫的在想什么?我还有个弟弟,所以我爸得有两套”
“啪!”
“你提莫还在睡,该搬砖了!”

正经点的,其实一句话来说:
继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展。
比如我有个构造函数A,然后又有个构造函数B,但是B想要使用A里的一些属性和方法,一种办法就是让我们自身化身为CV侠,复制粘贴一波。还有一种就是利用继承,我让B直接继承了A里的功能,这样我就能用它了。
今天要介绍的八种继承方式在目录中都已经列举出来了。
前七种都是ES5的继承方式,第八种是ES6之后的继承方式,因为class是ES6才出来的。
🐎耶~
都快比我争家产还复杂了。
不着急,从浅到深咱一个个来看。

  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上。

这看着不就是之前都讲到过的内容嘛?
就像是题目1.6和1.7一样(《封装篇(牛刀小试)》里的)。

所以现在你知道了吧,这种方式就叫做原型链继承。
将子类的原型对象指向父类的实例。
我们来写个伪代码,方便记忆:
Child.prototype = new Parent()
复制代码1.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()这种方式。
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和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可以看出来)

这…这看到没,压根就不需要记,想想霖呆呆出的这道变态的题面试的时候被问到脱口就来了。
2. instanceof
2.1 题目一
这道题主要是想介绍一个重要的运算符: 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这种东西,按照我图上的查找路线来查找就可以了 😁 ~
(如果你能看到这里,你就会发现霖呆呆的美术功底,不是一般的强)
[表情包害羞~]

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
复制代码判断的方式只要把原型链继承instanceof查找思维导图这张图反过来查找即可。
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 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’)
}
复制代码这时候就是好孩子了。
(哎,霖呆呆的产生可能就是第二种情况…)
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: ‘child1’, sex: ‘boy’, colors: [‘white’, ‘black’] }
复制代码我们发现修改child1.colors并不会影响到其它的实例(child2)耶。
这里的原因其实我们前面也说了:
使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类。
所以现在child1和child2现在分别有它们各自的colors了,就不共享了。
而且这种拷贝属于深拷贝,验证的方式是你可以把colors数组中的每一项改为一个对象,然后修改它看看。
function Parent () {
//…
this.colors = [{ title: ‘white’ }, { title: ‘black’ }]
}
复制代码因此我们可以得出构造继承的优点:

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

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
复制代码
sex、name属性都有这个我们都可以理解
getSex属于Child构造函数原型对象上的方法,我们可能是能用它的,这个也好理解
那getName呢?它属于父类构造函数原型对象上的方法,报错了?怎么滴?我子类不配使用你啊?

你确实是不配使用我。
你使用Parent.call(this, ‘good boy’)只不过是让你复制了一下我构造函数里的属性和方法,可没说能让你复制我原型对象的啊~年轻人,不要这么贪嘛。
所以我们可以看出构造继承一个最大的缺点,那就是:
小气!
“啪!”
"你给我正经点"😂
其实是:

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

“那不就是小气嘛…”
“…”

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检测到你和父类有关系的话,那就会对这些凭空产生的属性和方法有所质疑…)
因此构造继承第二个缺点是:

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

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

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

缺点:

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

(最后一个缺点其实有点没太理解…望解答)
4. 组合继承
既然原型链继承和构造继承都有这么多的缺点,那我们为何不阴阳结合,把它们组合在一起呢?
咦~
好像是个好想法。
把我们前面的伪代码拿来用用,想想该如何组合呢?
// 原型链继承
Child.prototype = new Parent()
// 构造继承
function Child () {
Parent.call(this, …arguments)
}
复制代码…思考中🤔…
看到这两段伪代码,我好像有所顿悟了,不就是按照伪代码里写的,把这两种继承组合在一起吗?
哇!这都被我猜中了,搜索一下组合继承的概念,果然就是这样。
组合继承的概念:
组合继承就是将原型链继承与构造函数继承组合在一起,从而发挥两者之长的一种继承模式。
思路:

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

基操:

通过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.constructor = Cat
guaiguai.proto.constructor = Cat
Cat.prototype.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的指向,那么出于编程习惯,我们最好将它修改为正确的构造函数。

4.3 题目三
(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

https://blog.csdn.net/A669MM/article/details/105000469
https://blog.csdn.net/A669MM/article/details/105000441
https://blog.csdn.net/A669MM/article/details/105000423
https://blog.csdn.net/A669MM/article/details/104999672
https://blog.csdn.net/A669MM/article/details/104999645
https://blog.csdn.net/A669MM/article/details/105034814
https://blog.csdn.net/A669MM/article/details/105034796
https://blog.csdn.net/A669MM/article/details/105034419
https://blog.csdn.net/A669MM/article/details/105034375
https://blog.csdn.net/A669MM/article/details/105034347

难度升级:
现在我想要在匿名函数外给A这个构造函数的原型对象中添加一个方法logB用以打印出this.b。
你首先想到的是不是B.prototype.logB = funciton() {}。
但是注意咯,我是要你在匿名函数外添加,而此时由于作用域的原因,我们在匿名函数外是访问不到A的,所以这样的做法就不可行了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值