➕ 原型和原型链
🧬 原型
在介绍原型的概念之前,让我们来看看原型所要解决的问题。如下图所示,通过构造函数User创建了三个实例,每个实例占用着单独的内存空间,它们的name和age属性是不同的,然而它们的writeName方法却完全相同。在这种情况下,相同逻辑的方法在内存中存在着三份,导致了空间的浪费。

在JavaScript中,构造函数用于模拟面向对象编程中的类的概念。在大多数情况下,所属一类的不同实例的相同方法逻辑是相似的。基于此,为了避免在每个实例上都分配函数对象的内存空间,可将这些实例方法从实例对象中抽离出来,使得不同实例对象共享相同的函数对象。
tip: 在很多基于类的面向对象语言中,不同实例的相同方法总是共享同一块内存空间的,例如C++、Java。
在JavaScript中,原型正是以上所述问题的解决方案。每个实例都有一个内置属性__proto__,属性指向的对象称为该实例的原型。
==那实例的原型是从何而来呢?==默认情况下,实例的原型为其创建时对应构造函数的prototype属性值。每个函数都会自动附带一个属性prototype,它默认指向一个空对象。除部分内置函数外,prototype属性是可以重新赋值的。
以构造函数User举例,具体关系图如下图所示。

tip:
- 有时候,
__proto__也被称为隐式原型,prototype也被称为显式原型。- 箭头函数没有自己的this值和原型,因此不能用作构造函数。
==那如何实现不同实例共享相同的函数对象呢?==当访问实例成员时,如果实例本身不存在该成员,JavaScript引擎会自动从原型中查找。基于此,将公共成员放到构造函数的prototype属性对象中,该构造函数创建出的实例便能够共享这些成员。
tip:
- 实例成员指的是实例身上的属性和方法。
- 一般情况下,我们只需要将成员方法放入原型中即可。
🔗 原型链
当通过构造函数创建实例后,实例的原型为构造函数的prototype属性值。而原型本身也是一个实例,默认情况下,它由构造函数Object创建产生。因此它也有原型,值为Object.prototype。
而Object.protytpe也是一个实例,==其是否也拥有原型呢?==答案是肯定的,它也存在__proto__属性,特殊的是,它的值为⭐null。

原型对象也会有自己的原型,逐渐构成了原型链。原型链终止于拥有null作为其原型的实例上,即Object.prototype。上图,User实例往上就是一条简单的原型链,也就是多个__proto__组成的链条。
其实,当访问实例成员时,若成员在实例中不存在,”JavaScript引擎会自动从原型中查找"这种说法是不准确的。准确来说,JavaScript引擎会沿着实例的原型链向上查找,直到到达原型链的尽头为止,即null。
tip:
Object.prototype属性是只读的,其值不能修改,但支持添加新的成员。
Object.prototype.__proto__是不可变的,值永远为null,当尝试修改时,会抛出错误。
在JavaScript中,函数也是实例,它们都是通过构造函数Function创建而来。因此函数也都拥有原型,值为Function.prototype,⭐包括Function函数本身。
而Function.prototype本身也是一个实例,它也是通过构造函数Object创建,所以其原型为Object.prototype。
下图展示了 JavaScript 中较完整的原型链结构。在这张图中,特别要注意粉色的线,因为它代表着一种特殊情况,需额外记忆和关注。

tip:
Function.prototype属性是只读的,其值不能修改,但支持添加新的成员。Function.prototype.__proto__是可变的。这意味着在面向对象概念中,我们可以在Fuction到Object中间加入继承类,从而构建更长的继承链。
🔌 API
Object.create
创建一个具有指定原型的新对象,并支持可选地为新对象添加指定的属性描述符。

Object.setPrototypeOf
设置对象的原型。

Object.getPrototypeOf
返回对象的原型。

tip: 尽管大多数主流的 JavaScript 运行环境都支持使用
__proto__属性来设置对象的原型,但我们也不推荐这样做。因为原型属性名是非标准的。在 ECMAScript 2015(ES6)之后的规范中,提供了Object.setPrototypeOf和Object.getPrototypeOf来访问对象的原型。

