__proto__
__proto__是[[Prototype]]的因历史原因而留下来的 getter/setter。
请注意,__proto__与内部的[[Prototype]]不一样。__proto__是[[Prototype]]的 getter/setter。稍后,我们将看到在什么情况下理解它们很重要,在建立对 JavaScript 语言的理解时,让我们牢记这一点。
__proto__属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数Object.getPrototypeOf/Object.setPrototypeOf来取代__proto__去 get/set 原型。 根据规范,__proto__必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。
现在,如果我们从 longEar 中读取一些它不存在的内容,JavaScript 会先在 rabbit 中查找,然后在 animal 中查找。
这里只有两个限制:
- 引用不能形成闭环。如果我们试图在一个闭环中分配
__proto__,JavaScript 会抛出错误。 __proto__的值可以是对象,也可以是null。而其他的类型都会被忽略。
几乎所有其他键/值获取方法都忽略继承的属性.
几乎所有其他键/值获取方法,例如Object.keys和Object.values等,都会忽略继承的属性。
它们只会对对象自身进行操作。不考虑 继承自原型的属性。
- 在 JavaScript 中,所有的对象都有一个隐藏的
[[Prototype]]属性,它要么是另一个对象,要么就是null。 - 我们可以使用
obj.__proto__访问它(历史遗留下来的 getter/setter,这儿还有其他方法,很快我们就会讲到)。 - 通过
[[Prototype]]引用的对象被称为“原型”。 - 如果我们想要读取
obj的一个属性或者调用一个方法,并且它不存在,那么 JavaScript 就会尝试在原型中查找它。 - 写/删除操作直接在对象上进行,它们不使用原型(假设它是数据属性,不是 setter)。
- 如果我们调用
obj.method(),而且method是从原型中获取的,this仍然会引用obj。因此,方法始终与当前对象一起使用,即使方法是继承的。 for..in循环在其自身和继承的属性上进行迭代。所有其他的键/值获取方法仅对对象本身起作用。
prototype
基本数据类型
最复杂的事情发生在字符串、数字和布尔值上。
正如我们记忆中的那样,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 String、Number 和 Boolean 被创建。它们提供给我们操作字符串、数字和布尔值的方法然后消失。
这些对象对我们来说是无形地创建出来的。大多数引擎都会对其进行优化,但是规范中描述的就是通过这种方式。这些对象的方法也驻留在它们的 prototype 中,可以通过 String.prototype、Number.prototype 和 Boolean.prototype 进行获取。
特殊值
null和undefined比较特殊。它们没有对象包装器,所以它们没有方法和属性。并且它们也没有相应的原型。
Object.setPrototypeOf and Object.getPrototypeOf
使用 obj.__proto__ 设置或读取原型被认为已经过时且不推荐使用(deprecated)了(已经被移至 JavaScript 规范的附录 B,意味着仅适用于浏览器)。
现代的获取/设置原型的方法有:
- Object.getPrototypeOf(obj) —— 返回对象
obj的[[Prototype]]。 - Object.setPrototypeOf(obj, proto) —— 将对象
obj的[[Prototype]]设置为proto。
__proto__不被反对的唯一的用法是在创建新对象时,将其用作属性:{ __proto__: ... }。 - Object.create(proto, [descriptors]) —— 利用给定的
proto作为[[Prototype]]和可选的属性描述来创建一个空对象。
原型历史
- 构造函数的
"prototype"属性自古以来就起作用。这是使用给定原型创建对象的最古老的方式。 - 之后,在 2012 年,
Object.create出现在标准中。它提供了使用给定原型创建对象的能力。但没有提供 get/set 它的能力。一些浏览器实现了非标准的__proto__访问器,以为开发者提供更多的灵活性。 - 之后,在 2015 年,
Object.setPrototypeOf和Object.getPrototypeOf被加入到标准中,执行与__proto__相同的功能。由于__proto__实际上已经在所有地方都得到了实现,但它已过时,所以被加入到该标准的附件 B 中,即:在非浏览器环境下,它的支持是可选的。 - 之后,在 2022 年,官方允许在对象字面量
{...}中使用__proto__(从附录 B 中移出来了),但不能用作 getter/setterobj.__proto__(仍在附录 B 中)。
如果速度很重要,就请不要修改已存在的对象的 [[Prototype]]
从技术上来讲,我们可以在任何时候 get/set [[Prototype]]。但是通常我们只在创建对象的时候设置它一次,自那之后不再修改:rabbit 继承自 animal,之后不再更改。
并且,JavaScript 引擎对此进行了高度优化。用 Object.setPrototypeOf 或 obj.__proto__= “即时”更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化。因此,除非你知道自己在做什么,或者 JavaScript 的执行速度对你来说完全不重要,否则请避免使用它。
原型只能是对象或者 null
__proto__ 属性很特殊:它必须是一个对象或者 null。字符串不能成为原型。这就是为什么将字符串赋值给 __proto__ 会被忽略。
创建无原型对象
Object.create(null) 或 {__proto__: null} 创建的无原型的对象。
对象会从 Object.prototype 继承内建的方法和 __proto__ getter/setter,会占用相应的键,且可能会导致副作用。原型为 null 时,对象才真正是空的。
[[HomeObject]]
当一个函数被定义为类或者对象方法时,它的 [[HomeObject]] 属性就成为了该对象。
然后 super 使用它来解析(resolve)父原型及其方法。
应该是方法而不是函数属性
[[HomeObject]] 是为类和普通对象中的方法定义的。但是对于对象而言,方法必须确切指定为 method(),而不是 "method: function()"。
这个差别对我们来说可能不重要,但是对 JavaScript 来说却非常重要。
在下面的例子中,使用非方法(non-method)语法进行了比较。未设置 [[HomeObject]] 属性,并且继承无效:
Symbol.species
以给类添加一个特殊的静态 getter Symbol.species。如果存在,则应返回 JavaScript 在内部用来在 map 和 filter 等方法中创建新实体的 constructor。
如果我们希望像 map 或 filter 这样的内建方法返回常规数组,我们可以在 Symbol.species 中返回 Array,就像这样:
内建类没有静态方法继承
内建对象有它们自己的静态方法,例如 Object.keys,Array.isArray 等。
如我们所知道的,原生的类互相扩展。例如,Array 扩展自 Object。
Symbol.hasInstance
instanceof 原理,obj instanceof Class 算法的执行过程大致如下:
- 如果这儿有静态方法
Symbol.hasInstance,那就直接调用这个方法:
例如: - 大多数 class 没有
Symbol.hasInstance。在这种情况下,标准的逻辑是:使用obj instanceof Class检查Class.prototype是否等于obj的原型链中的原型之一,即只需要判断, 实例对象__proto__指针是否指向类的原型换句话说就是,一个接一个地比较:
这里还要提到一个方法 objA.isPrototypeOf(objB),如果 objA 处在 objB 的原型链中,则返回 true。所以,可以将 obj instanceof Class 检查改为 Class.prototype.isPrototypeOf(obj)。
但是 Class 的 constructor 自身是不参与检查的!检查过程只和原型链以及 Class.prototype 有关。
创建对象后,如果更改 prototype 属性,可能会导致有趣的结果。
就像这样:
Object.prototype.toString.call
内建的 toString 方法可以被从对象中提取出来,并在任何其他值的上下文中执行。其结果取决于该值。
- 对于 number 类型,结果是
[object Number] - 对于 boolean 类型,结果是
[object Boolean] - 对于
null:[object Null] - 对于
undefined:[object Undefined] - 对于数组:
[object Array] - ……等(可自定义)
Symbol.toStringTag
可以使用特殊的对象属性 Symbol.toStringTag 自定义对象的 toString 方法的行为。
例如:
Object.create()
Object.create() 可能出现的潜在的问题
令你使用的对象不继承 Object.prototype 原型的方法也可以防止原型污染攻击。如果恶意脚本向 Object.prototype 添加了一个属性,这个属性将能够被程序中的每一个对象所访问,而以 null 为原型的对象则不受影响。
用 Object.create() 实现类式继承
Object.create() 的第二个参数
propertiesObject: 如果该参数被指定且不为 undefined,则该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。这些属性对应于 Object.defineProperties() 的第二个参数。