原型链
原型链🤔
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,则会导致构造函数的原型引用整个更改,而之前构造的对象仍保持之前的引用,因此不会改变
怎么样,是不是和闭包极其的相似😏