➕ 原型和原型链
🧬 原型
在介绍原型的概念之前,让我们来看看原型所要解决的问题。如下图所示,通过构造函数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
来访问对象的原型。