原型链

原型链🤔

1. 原型链的形成

原型链js 实现继承的重要方式,也是 js 与其他语言与众不同的地方之一, js 中,原型链的形成方式如下:

  1. js中每个函数都有一个 原型对象, 这个对象只能在 new 构造时才能使用,当作为普通函数时,访问这个对象将会得到 undefined🪨
  2. 每个原型对象中包含着 指向上一个原型对象的指针, 当自身对象没有这个属性时, 会根据这个指针寻找上一层对象的相应属性 ,于是就形成了 链式结构

2. 原型对象的组成🤐

可能看完上面的解释大家还有些迷糊,没关系,在这一小节,我会跟大家详细介绍 原型对象 这个玩意。

初始的原型对象中有三种属性:

1
2
3
4
5
6
7
8
GGBond.prototype = {
__proto__: Object.prototype,
constructor: GGBond,
super_bang_bang_tang: function() {
console.log("超级棒棒糖");
}
fei_fei_gong_zhu: 'wife'
}
  • 一种叫做 __proto__, 这个就是刚刚所说指向上级的指针,当自身的prototype找不到相应属性时,就要顺着这个指针寻找上一级里的属性🤣

    __proto__是个很有意思的变量,如果你尝试打印它:

    1
    console.log(GGBond.prototype.__proto__);

    你将会得到 {},但它实际上并不为空对象,如果你尝试以下比较:

    1
    console.log(GGBond.prototype.__proto__ === Object.prototype);

    那你会惊奇的发现它会返回 true😍, 这是因为__proto__是一个内部属性,所以某些时候会被限制显示。

    因此,在 ES6中,更推荐使用新增的 Object.getPrototypeOfObject.getPrototypeOf方法而不是直接访问__proto__

    有意思的是,__proto__ 这个属性本来就不是 ES 的规范,直到 ES6 才被正式引入,但是官方在 ES6 又给出了它的替代方案(实际上就是封装了一小下😡),属于是生不逢时了

  • 一种叫做constructor,它指向自身,主要是用来判别是否为某个构造函数的实例,但是个人感觉它的功能快被某个Symbol替代了,还记得是哪个吗😏,不记得了记得去看看我之前的文章。

    个人感觉网上所说的它是维护原型链的方法不是很贴切😵,他们所说的维护原型链,也就是让继承更规范,类型识别更加方便。实际上constructor根本不会影响继承,也不会影响原型链上下的东西,感觉叫做类型标识也比维护原型链更好一些🤐

  • 最后就是咱们挂载到原型对象上的属性和方法了,在上面, 就是super_bang_bang_tangfei_fei_gong_zhu

    注意:

    有些属性并不会被挂载到原型链上,也就是说,原型链上不会有所有的属性。🐶具体哪些不能在之后会有介绍

有没有感觉这个过程很像咱们亲爱的 闭包 😏

3. 原型对象最后去哪了?

由上面,咱们可以知道,原型对象是函数上才有的对象,那么如果咱们构造一个对象,那构造函数的原型对象会去哪里呢?🤔

还是以 GGBond 为例:

1
2
3
4
const dead_pig = new GGBond();
for(let key in dead_pig) {
console.log(key);
}

假设所有属性都是可枚举的,那么上面的输出会变成:

1
2
3
> constructor
super_bang_bang_tang
fei_fei_gong_zhu

__proto__为什么不在,看看上边,写了好长一段嘞😵

由此可见,原型对象在构建成对象之后,会自动散开constructor__proto__,变成对象的两个属性,而其他则被舍弃,这时候,如果你要访问某个当前对象没有的属性,那么他就会顺着__proto__上溯去找咯

注意:

这里有 fei_fei_gong_zhusuper_bang_bang_tang是因为并没有把他们挂载到原型链上,而是挂载到实例上,原型链上的属性并不会被散开到对象中

4.你已经学会原型链了,来试试写出ES6的class吧😎

ES6class其实就是一个语法糖,是对原型链操作的简单封装,接下来,让我们简单实现以下 classextends 操作吧:

还是以GGBond为例,先简单定义一个DeadPig构造函数和一个GGBond构造函数:

1
2
3
4
5
6
7
8
function DeadPig() {
this.category = "pig"
this.food = "vegetable"
}
function GGBond() {
this.name = "GGBond"
this.food = "超级棒棒糖"
}

接下来,要让GGBond构造函数继承DeadPig函数,该怎么办呢😏


那肯定是要修改GGBondprototypeDeadPig对不对,开干!

且慢,咱们要想实现继承,首先要解决两个问题:

  • 子类要有父类定义的共用属性
  • 子类的改动不影响父类

要想实现这两点,就肯定不能用父类的直接引用,而要用间接引用,而什么是间接引用呢?构造出来的实例就是一个不错的选择,这样主要有两个好处:

  • 子类不会影响父类
  • 子类实例上的某些方法也会被带进来,范围更广

于是就有了下面的操作:

1
GGBond.prototype = new DeadPig();

让我们来看看这个操作究竟都做了什么:

首先是创建了一个DeadPig对象,大概张这样:

1
2
3
4
5
6
{
__proto__: Object.prototype,
constructor: DeadPig,
category: "pig",
food: "vegetable"
}

之后把这个对象赋值给 GGBond.prototype,这时GGBond大概长这样:

1
2
3
4
5
6
7
8
{
prototype: {
__proto__: Object.prototype,
constructor: DeadPig,
category: "pig",
food: "vegetable"
}
}

这个时候就已经基本完成继承了,但是还有个小问题,就是GGBondconstructor还是DeadPig,这很让人恼火.😡

如果别人访问GGBond实例的constructor,就会得到DeadPig,这还不是最烦人的,更重要的是**如果GGBond也被继承,那么它的子类的constructor**也会是DeadPig,来看例子:

1
2
3
4
5
6
7
//如果GGBond也有实例,那应该长这样
{
__proto__: Object.prototype,
constructor: DeadPig,
food: "超级棒棒糖",
name: "GGBond",
}

如果它再被继承,那么被继承的构造函数应该长这样:

1
2
3
4
5
6
7
8
9
{
prototype: {
__proto__: Object.prototype,
constructor: DeadPig,
food: "超级棒棒糖",
name: "GGBond",
}
// ...其他乱七八糟的属性
}

那它的constructor自然而然的也是DeadPig

那么如何修改constructor呢?很简单,暴力修改就行了😡

1
2
3
4
Object.defineProperty(GGBond.prototype, "constructor", {
enumberable: false,
value: GGBond
})

这下就圆满咯🤣但还有一些小细节要搞

5.深入理解构造函数和Class

1
2
3
function GGBond() {
this.name = "ggb"
}

这里的 name 属性是挂载到 实例 上的,而非原型链。因此上面的示例中:

image1

可以看到GGBond.prototype内并没有自身的属性,如果想让属性存到原型链中,可以使用: Object.defineProperty(GGBond.prototype, 'name', {value: value}) 或者 GGBond.prototype[name] = value 来实现


1
2
3
4
5
6
7
8
class GGBond {
constructor() {},
name: 1,
value: 2,
getValue() {

}
}

但是如果到了Class中, name这种属性还是挂载到示例上,而getValue这种方法,则会直接挂载到原型链上


还有一件很有意思的事情:

如果你修改prototype的一部分,并没有完全修改的话,那么在修改之前创建的实例会随之一起变动

但如果完全修改,那么在修改之前创建的实例会像闭包一样不受影响

例如:

1
2
3
4
5
6
7
8
9
10
const ggb = new GGBond();
GGBond.prototype.status = "dead"
console.log(ggb.status)
// > dead

GGBond.prototype = {
status: "alive"
}
console.log(ggb.status)
// > dead

出现这种情况是因为当修改一部分属性的时候,构造函数的原型引用仍未改变,只是修改了引用上的几个方法或属性,而js又是动态查找,所以会同步到所有构造出的对象。而一旦修改了整个prrototype,则会导致构造函数的原型引用整个更改,而之前构造的对象仍保持之前的引用,因此不会改变

怎么样,是不是和闭包极其的相似😏


原型链
http://baidu.com/2023/11/17/proto/
作者
KB
发布于
2023年11月17日
许可协议