在学习原型与原型链之前,我认为你需要对 JavaScript 的内存模型(堆内存)足够的了解,否则你将无法理解原型以及原型链。

JavaScript 是一种基于原型的语言 (prototype-based language) ,在 JavaScript 中每一个对象中都有一个属性指向原型对象,或者说与其相关联。

原型 Prototype

原型的本质

在 JavaScript 中一个原型(prototype)就是一个对象,因此原型对象和原型实际上是一个意思。

我们都知道,对象是存储在堆(heap)内存当中的一块区域,原型对象也不例外。

接下来,我们通过 Array.prototype 这个原型对象(Prototype Object)来一步一步的揭示原型。

图解原型与原型链 - 图1
通过内存图看出,Array 构造函数的原型对象存在于一块堆内存空间中,且内存地址为 0x666

这个地址值是随便编的,此处只是举一个例子

我们已经知道了原型对象在内存中的位置。那么原型对象又在那些地方被引用了呢?

构造函数的 prototype

第一个引用到原型对象的地方就是构造函数(Constructor Function)

我们知道,JavaScript 中函数也是一个对象,因此构造函数也会出现在堆内存当中。而构造函数作为一个对象,其中有一个名为 prototype 的属性,该属性存储着一个引用值,其指向的地址为 0x666 也就是 Array 构造函数的原型对象。
图解原型与原型链 - 图2

注意:只有函数才会有 prototype 属性,而非构造函数中虽然存在 prototype 属性,但是没什么作用。

定理

我们直接粗暴的给出定理:什么构造函数就对应着什么原型对象。

比如,Array 这个构造函数所对应的原型对象便是 Array.prototype ,而 Object 这个构造函数所对应的原型对象便是 Object.prototype

实例的 proto

另一个使用到了原型对象的地方便是实例对象中的 __proto__ 属性,内存图如下。
图解原型与原型链 - 图3

后文将会提到 __proto__[[prototype]] 之间的关系,现在可以将其看做同一个东西的不同写法。

定理

实例中的 __proto__ 的指向逻辑:谁构造的实例,实例中的 __proto__ 属性就指向谁的原型对象。

  1. const arr = new Array(1, 2);
  2. const obj = new Object();

比如上述代码,arr 是 Array 构造函数的实例,因此 arr 的 __proto__ 指向的就是 Array.prototype ,而 obj 是 Object 构造函数的实例,因此 obj 的 __proto__ 指向的就是 Object.prototype

注意:Object.prototype.proto 存在特殊情况,其对应的值为 null,后文会用内存图的形式表达。

原型对象中的 constructor

我们将代码和内存图结合起来,并且增加点东西。

  1. const arr = new Array(1, 2, 3);

上述代码的内存图如下:
图解原型与原型链 - 图4
这一次我们添加了两个细节:

  1. 实例对象 arr 中的全部属性
  2. Array.prototype 实例对象中的 constructor 属性

如图所示,在原型对象中,存在着一个名为 constructor 的属性,其存储着一个引用值,引用指向的地址为 0x222 也就是 Array 构造函数的内存地址。

定理

每一个原型对象(Prototype Object)上,都存在一个名为 constructor 的属性,该属性指向的值一定是一个构造函数,且该构造函数中的 prototype 属性一定指回该原型对象。

可能有点绕口,你也可以直接看下面这段代码并结合内存图来理解。

  1. Object === Object.prototype.constructor // true
  2. Array === Array.prototype.constructor // true

图解原型与原型链 - 图5

小结

我们复习一下,上面说到的几个定理:

  1. 什么构造函数就对应着什么原型对象。
  2. 谁构造的实例,实例中的 __proto__ 属性就指向谁的原型对象。
  3. 每一个原型对象(Prototype Object)上,都存在一个名为 constructor 的属性,该属性指向的值一定是一个构造函数,且该构造函数中的 prototype 属性一定指回该原型对象。

我们可以配合下图理解:

图解原型与原型链 - 图6

原型链

原型链并不是什么高级的概念,我们将继续完善内存图,等我们完善好内存图的时候,原型链就自然而然的出来了。

构造函数的 proto

构造函数本质是什么?也是一个对象,因此也存在 __proto__ 属性。

因为是构造函数是一个函数,也就是说函数是 Function 构造函数的实例,也就是说构造函数是 Function 构造函数的实例。

所以,构造函数的 __proto__ 指向 Function.prototype

图解原型与原型链 - 图7
在上图中,我们又补充了 Function 构造函数在内存图中的情况。

我们依然根据上面的定律得出:

  1. Function 构造函数的 prototype 指向的是 Function.prototype
  2. 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

下面,我们画出原型图
图解原型与原型链 - 图8
新增的细节:

  1. Object.prototype 在内存中的示意图
  2. Object 构造函数在内存中的示意图
  3. Array.prototype__proto__ 属性的引用
  4. Function.prototype__proto__ 属性的引用
  5. Object.prototypeconstructor 属性的引用

一样,Object.prototype 也是一个对象,因此也会存在 __proto__ 属性,但是这里比较特殊,其对应的值是 null
图解原型与原型链 - 图9
那讲到这里,好像也没说什么是原型链。不过原型链其实已经出现了。我们在图中使用红色加粗的线来表示其中的一条原型链。

原型链通过对象的 __proto__ 属性相连接,最终在内存图中构成了一个链条,就是所谓的原型链。

  1. 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__,实际使用中建议使用规范提供的方法,修改或访问原型。

总结

三个定理和一个特殊情况:

定理:

  1. 什么构造函数就对应着什么原型对象。
  2. 谁构造的实例,实例中的 __proto__ 属性就指向谁的原型对象。
  3. 每一个原型对象(Prototype Object)上,都存在一个名为 constructor 的属性,该属性指向的值一定是一个构造函数,且该构造函数中的 prototype 属性一定指回该原型对象。

特殊情况:

  1. Object.prototype 指向 null

最后还需要注意理解为什么 Function 的 __proto__ 指向 Function.prototype,以及所谓鸡生蛋蛋生鸡的问题。

可以参考 hax 在知乎上的回答: https://www.zhihu.com/question/31333084/answer/152086175

最后,下面这种图非常好的描述了原型链在内存中的情况。

image.png