在学习原型与原型链之前,我认为你需要对 JavaScript 的内存模型(堆内存)足够的了解,否则你将无法理解原型以及原型链。
JavaScript 是一种基于原型的语言 (prototype-based language) ,在 JavaScript 中每一个对象中都有一个属性指向原型对象,或者说与其相关联。
原型 Prototype
原型的本质
在 JavaScript 中一个原型(prototype)就是一个对象,因此原型对象和原型实际上是一个意思。
我们都知道,对象是存储在堆(heap)内存当中的一块区域,原型对象也不例外。
接下来,我们通过 Array.prototype
这个原型对象(Prototype Object)来一步一步的揭示原型。
通过内存图看出,Array 构造函数的原型对象存在于一块堆内存空间中,且内存地址为 0x666
。
这个地址值是随便编的,此处只是举一个例子
我们已经知道了原型对象在内存中的位置。那么原型对象又在那些地方被引用了呢?
构造函数的 prototype
第一个引用到原型对象的地方就是构造函数(Constructor Function)。
我们知道,JavaScript 中函数也是一个对象,因此构造函数也会出现在堆内存当中。而构造函数作为一个对象,其中有一个名为 prototype
的属性,该属性存储着一个引用值,其指向的地址为 0x666
也就是 Array 构造函数的原型对象。
注意:只有函数才会有
prototype
属性,而非构造函数中虽然存在prototype
属性,但是没什么作用。
定理
我们直接粗暴的给出定理:什么构造函数就对应着什么原型对象。
比如,Array 这个构造函数所对应的原型对象便是 Array.prototype
,而 Object 这个构造函数所对应的原型对象便是 Object.prototype
。
实例的 proto
另一个使用到了原型对象的地方便是实例对象中的 __proto__
属性,内存图如下。
后文将会提到
__proto__
与[[prototype]]
之间的关系,现在可以将其看做同一个东西的不同写法。
定理
实例中的 __proto__
的指向逻辑:谁构造的实例,实例中的 __proto__
属性就指向谁的原型对象。
const arr = new Array(1, 2);
const obj = new Object();
比如上述代码,arr 是 Array 构造函数的实例,因此 arr 的 __proto__
指向的就是 Array.prototype
,而 obj 是 Object 构造函数的实例,因此 obj 的 __proto__
指向的就是 Object.prototype
注意:Object.prototype.proto 存在特殊情况,其对应的值为 null,后文会用内存图的形式表达。
原型对象中的 constructor
我们将代码和内存图结合起来,并且增加点东西。
const arr = new Array(1, 2, 3);
上述代码的内存图如下:
这一次我们添加了两个细节:
- 实例对象 arr 中的全部属性
Array.prototype
实例对象中的constructor
属性
如图所示,在原型对象中,存在着一个名为 constructor
的属性,其存储着一个引用值,引用指向的地址为 0x222
也就是 Array 构造函数的内存地址。
定理
每一个原型对象(Prototype Object)上,都存在一个名为 constructor
的属性,该属性指向的值一定是一个构造函数,且该构造函数中的 prototype
属性一定指回该原型对象。
可能有点绕口,你也可以直接看下面这段代码并结合内存图来理解。
Object === Object.prototype.constructor // true
Array === Array.prototype.constructor // true
小结
我们复习一下,上面说到的几个定理:
- 什么构造函数就对应着什么原型对象。
- 谁构造的实例,实例中的
__proto__
属性就指向谁的原型对象。 - 每一个原型对象(Prototype Object)上,都存在一个名为
constructor
的属性,该属性指向的值一定是一个构造函数,且该构造函数中的prototype
属性一定指回该原型对象。
我们可以配合下图理解:
原型链
原型链并不是什么高级的概念,我们将继续完善内存图,等我们完善好内存图的时候,原型链就自然而然的出来了。
构造函数的 proto
构造函数本质是什么?也是一个对象,因此也存在 __proto__
属性。
因为是构造函数是一个函数,也就是说函数是 Function 构造函数的实例,也就是说构造函数是 Function 构造函数的实例。
所以,构造函数的 __proto__
指向 Function.prototype
。
在上图中,我们又补充了 Function 构造函数在内存图中的情况。
我们依然根据上面的定律得出:
- Function 构造函数的
prototype
指向的是Function.prototype
。 - Function 构造函数本质是 Function 构造函数的实例,因此
__proto__
指向Function.prototype
你可能会发现,Function 是一个鸡生蛋蛋生鸡的问题。其实并不是,其实并不是,Function 作为 JavaScript 内置的对象,并不是被构造出来的。
可以参考 hax 在知乎上的回答: https://www.zhihu.com/question/31333084/answer/152086175
构造函数原型对象的 proto
现在我们再把目光聚焦在 Array.prototype
上,也就是 Array 的原型对象,其本质也是一个对象,因此也会存在一个名为 __proto__
的属性,其指向的地址是构造该实例的构造函数的原型对象的地址。因为该实例是一个普通的对象,因此其构造函数是 Object,而 Object 的原型对象是 Object.prototype
。
下面,我们画出原型图
新增的细节:
Object.prototype
在内存中的示意图- Object 构造函数在内存中的示意图
Array.prototype
中__proto__
属性的引用Function.prototype
中__proto__
属性的引用Object.prototype
中constructor
属性的引用
一样,Object.prototype 也是一个对象,因此也会存在 __proto__
属性,但是这里比较特殊,其对应的值是 null
。
那讲到这里,好像也没说什么是原型链。不过原型链其实已经出现了。我们在图中使用红色加粗的线来表示其中的一条原型链。
原型链通过对象的 __proto__
属性相连接,最终在内存图中构成了一个链条,就是所谓的原型链。
arr.__proto__ -> Array.prototype.__proto__ -> Object.prototype.__proto__ -> null
关于 [[prototype]]
遵循 ECMAScript 标准,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 proto。 —— MDN
[[prototype]] 是规范中的定义,并且提供了方法去设置和修改 [[prototype]]。而 __proto__
是浏览器实现的属性,也可以做到同样的操作。
为了方便演示,我直接使用的 __proto__
,实际使用中建议使用规范提供的方法,修改或访问原型。
总结
三个定理和一个特殊情况:
定理:
- 什么构造函数就对应着什么原型对象。
- 谁构造的实例,实例中的
__proto__
属性就指向谁的原型对象。 - 每一个原型对象(Prototype Object)上,都存在一个名为
constructor
的属性,该属性指向的值一定是一个构造函数,且该构造函数中的prototype
属性一定指回该原型对象。
特殊情况:
Object.prototype
指向null
。
最后还需要注意理解为什么 Function 的 __proto__
指向 Function.prototype
,以及所谓鸡生蛋蛋生鸡的问题。
可以参考 hax 在知乎上的回答: https://www.zhihu.com/question/31333084/answer/152086175
最后,下面这种图非常好的描述了原型链在内存中的情况。