原型链
原型链🤔
1. 原型链的形成
原型链
是 js
实现继承的重要方式,也是 js
与其他语言与众不同的地方之一, js
中,原型链的形成方式如下:
js
中每个函数都有一个原型对象
, 这个对象只能在new
构造时才能使用,当作为普通函数时,访问这个对象将会得到undefined
🪨- 每个原型对象中包含着
指向上一个原型对象的指针
, 当自身对象没有这个属性时, 会根据这个指针寻找上一层对象的相应属性 ,于是就形成了链式结构
2. 原型对象的组成🤐
可能看完上面的解释大家还有些迷糊,没关系,在这一小节,我会跟大家详细介绍 原型对象
这个玩意。
初始的原型对象
中有三种属性:
1 |
|
一种叫做
__proto__
, 这个就是刚刚所说指向上级的指针,当自身的prototype
找不到相应属性时,就要顺着这个指针寻找上一级里的属性🤣__proto__
是个很有意思的变量,如果你尝试打印它:1
console.log(GGBond.prototype.__proto__);
你将会得到
{}
,但它实际上并不为空对象,如果你尝试以下比较:1
console.log(GGBond.prototype.__proto__ === Object.prototype);
那你会惊奇的发现它会返回
true
😍, 这是因为__proto__
是一个内部属性,所以某些时候会被限制显示。因此,在
ES6
中,更推荐使用新增的Object.getPrototypeOf
和Object.getPrototypeOf
方法而不是直接访问__proto__
有意思的是,
__proto__
这个属性本来就不是ES
的规范,直到ES6
才被正式引入,但是官方在ES6
又给出了它的替代方案(实际上就是封装了一小下😡),属于是生不逢时了一种叫做
constructor
,它指向自身,主要是用来判别是否为某个构造函数的实例,但是个人感觉它的功能快被某个Symbol
替代了,还记得是哪个吗😏,不记得了记得去看看我之前的文章。个人感觉网上所说的它是
维护原型链的方法
不是很贴切😵,他们所说的维护原型链
,也就是让继承更规范,类型识别更加方便。实际上constructor
根本不会影响继承,也不会影响原型链上下的东西,感觉叫做类型标识
也比维护原型链
更好一些🤐最后就是咱们挂载到原型对象上的属性和方法了,在上面, 就是
super_bang_bang_tang
和fei_fei_gong_zhu
注意:
有些属性并不会被挂载到原型链上,也就是说,原型链上不会有所有的属性。🐶具体哪些不能在之后会有介绍
有没有感觉这个过程很像咱们亲爱的 闭包
😏
3. 原型对象最后去哪了?
由上面,咱们可以知道,原型对象
是函数上才有的对象,那么如果咱们构造一个对象,那构造函数的原型对象
会去哪里呢?🤔
还是以 GGBond
为例:
1 |
|
假设所有属性都是可枚举
的,那么上面的输出会变成:
1 |
|
__proto__
为什么不在,看看上边,写了好长一段嘞😵
由此可见,原型对象
在构建成对象之后,会自动散开constructor
和__proto__
,变成对象的两个属性,而其他则被舍弃,这时候,如果你要访问某个当前对象没有的属性,那么他就会顺着__proto__
上溯去找咯
注意:
这里有
fei_fei_gong_zhu
和super_bang_bang_tang
是因为并没有把他们挂载到原型链上,而是挂载到实例上,原型链上的属性并不会被散开到对象中
4.你已经学会原型链了,来试试写出ES6的class吧😎
ES6
的class
其实就是一个语法糖,是对原型链操作的简单封装,接下来,让我们简单实现以下 class
中 extends
操作吧:
还是以GGBond
为例,先简单定义一个DeadPig
构造函数和一个GGBond
构造函数:
1 |
|
接下来,要让GGBond
构造函数继承DeadPig
函数,该怎么办呢😏
那肯定是要修改GGBond
的prototype
为DeadPig
对不对,开干!
且慢,咱们要想实现继承,首先要解决两个问题:
- 子类要有父类定义的共用属性
- 子类的改动不影响父类
要想实现这两点,就肯定不能用父类的直接引用,而要用间接引用,而什么是间接引用呢?构造出来的实例
就是一个不错的选择,这样主要有两个好处:
- 子类不会影响父类
- 子类实例上的某些方法也会被带进来,范围更广
于是就有了下面的操作:
1 |
|
让我们来看看这个操作究竟都做了什么:
首先是创建了一个DeadPig
对象,大概张这样:
1 |
|
之后把这个对象赋值给 GGBond.prototype
,这时GGBond
大概长这样:
1 |
|
这个时候就已经基本完成继承了,但是还有个小问题,就是GGBond
的constructor
还是DeadPig
,这很让人恼火.😡
如果别人访问GGBond
实例的constructor
,就会得到DeadPig
,这还不是最烦人的,更重要的是**如果GGBond
也被继承,那么它的子类的constructor
**也会是DeadPig
,来看例子:
1 |
|
如果它再被继承,那么被继承的构造函数应该长这样:
1 |
|
那它的constructor
自然而然的也是DeadPig
那么如何修改constructor
呢?很简单,暴力修改就行了😡
1 |
|
这下就圆满咯🤣但还有一些小细节要搞
5.深入理解构造函数和Class
1 |
|
这里的 name
属性是挂载到 实例
上的,而非原型链。因此上面的示例中:
可以看到GGBond.prototype
内并没有自身的属性,如果想让属性存到原型链中,可以使用: Object.defineProperty(GGBond.prototype, 'name', {value: value})
或者 GGBond.prototype[name] = value
来实现
1 |
|
但是如果到了Class
中, name
这种属性还是挂载到示例上,而getValue
这种方法,则会直接挂载到原型链上
还有一件很有意思的事情:
如果你修改prototype
的一部分,并没有完全修改的话,那么在修改之前创建的实例会随之一起变动
但如果完全修改,那么在修改之前创建的实例会像闭包
一样不受影响
例如:
1 |
|
出现这种情况是因为当修改一部分属性的时候,构造函数的
原型引用
仍未改变,只是修改了引用上的几个方法或属性,而js
又是动态查找,所以会同步到所有构造出的对象。而一旦修改了整个prrototype
,则会导致构造函数的原型引用
整个更改,而之前构造的对象仍保持之前的引用,因此不会改变
怎么样,是不是和闭包
极其的相似😏