原型链
Published by powerfulyang on Oct 6, 2022
__proto__
__proto__
是[[Prototype]]
的因历史原因而留下来的 getter/setter。
请注意,__proto__
与内部的[[Prototype]]
不一样。__proto__
是[[Prototype]]
的 getter/setter。稍后,我们将看到在什么情况下理解它们很重要,在建立对 JavaScript 语言的理解时,让我们牢记这一点。
__proto__
属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数Object.getPrototypeOf/Object.setPrototypeOf
来取代__proto__
去 get/set 原型。 根据规范,__proto__
必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
let animal = { eats: true, walk() { alert("Animal walk"); } }; let rabbit = { jumps: true, __proto__: animal }; let longEar = { earLength: 10, __proto__: rabbit }; // walk 是通过原型链获得的 longEar.walk(); // Animal walk alert(longEar.jumps); // true(从 rabbit)
现在,如果我们从 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
1 2 3
let obj = {}; alert(obj.__proto__ === Object.prototype); // true
1 2 3 4 5 6 7 8 9 10
let arr = [1, 2, 3]; // 它继承自 Array.prototype? alert( arr.__proto__ === Array.prototype ); // true // 接下来继承自 Object.prototype? alert( arr.__proto__.__proto__ === Object.prototype ); // true // 原型链的顶端为 null。 alert( arr.__proto__.__proto__.__proto__ ); // null
1 2 3 4
function f() {} alert(f.__proto__ == Function.prototype); // true alert(f.__proto__.__proto__ == Object.prototype); // true,继承自 Object
基本数据类型
最复杂的事情发生在字符串、数字和布尔值上。
正如我们记忆中的那样,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 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
1 2 3 4 5 6
let obj = {}; let key = prompt("What's the key?", "__proto__"); obj[key] = "some value"; alert(obj[key]); // [object Object],并不是 "some value"!
1 2 3 4 5 6
let obj = {}; let key = prompt("What's the key?", "__proto__"); obj[key] = new String("some value"); alert(obj[key]); // 这里却会是 "some value"!
__proto__
属性很特殊:它必须是一个对象或者 null
。字符串不能成为原型。这就是为什么将字符串赋值给 __proto__
会被忽略。
创建无原型对象
Object.create(null)
或 {__proto__: null}
创建的无原型的对象。
对象会从 Object.prototype
继承内建的方法和 __proto__
getter/setter,会占用相应的键,且可能会导致副作用。原型为 null
时,对象才真正是空的。
[[HomeObject]]
当一个函数被定义为类或者对象方法时,它的 [[HomeObject]]
属性就成为了该对象。
然后 super
使用它来解析(resolve)父原型及其方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
let animal = { name: "Animal", eat() { // animal.eat.[[HomeObject]] == animal alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, name: "Rabbit", eat() { // rabbit.eat.[[HomeObject]] == rabbit super.eat(); } }; let longEar = { __proto__: rabbit, name: "Long Ear", eat() { // longEar.eat.[[HomeObject]] == longEar super.eat(); } }; // 正确执行 longEar.eat(); // Long Ear eats.
应该是方法而不是函数属性
[[HomeObject]]
是为类和普通对象中的方法定义的。但是对于对象而言,方法必须确切指定为 method()
,而不是 "method: function()"
。
这个差别对我们来说可能不重要,但是对 JavaScript 来说却非常重要。
在下面的例子中,使用非方法(non-method)语法进行了比较。未设置 [[HomeObject]]
属性,并且继承无效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
let animal = { eat: function() { // 这里是故意这样写的,而不是 eat() {... // ... } }; let rabbit = { __proto__: animal, eat: function() { super.eat(); } }; rabbit.eat(); // 错误调用 super(因为这里没有 [[HomeObject]])
Symbol.species
以给类添加一个特殊的静态 getter Symbol.species
。如果存在,则应返回 JavaScript 在内部用来在 map
和 filter
等方法中创建新实体的 constructor
。
如果我们希望像 map
或 filter
这样的内建方法返回常规数组,我们可以在 Symbol.species
中返回 Array
,就像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class PowerArray extends Array { isEmpty() { return this.length === 0; } // 内建方法将使用这个作为 constructor static get [Symbol.species]() { return Array; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false // filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组 let filteredArr = arr.filter(item => item >= 10); // filteredArr 不是 PowerArray,而是 Array alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function
内建类没有静态方法继承
内建对象有它们自己的静态方法,例如 Object.keys
,Array.isArray
等。
如我们所知道的,原生的类互相扩展。例如,Array
扩展自 Object
。
Symbol.hasInstance
obj instanceof Class
算法的执行过程大致如下:
- 如果这儿有静态方法
Symbol.hasInstance
,那就直接调用这个方法:
例如:1 2 3 4 5 6 7 8 9
// 设置 instanceOf 检查 // 并假设具有 canEat 属性的都是 animal class Animal { static [Symbol.hasInstance](obj) { if (obj.canEat) return true; } } let obj = { canEat: true }; alert(obj instanceof Animal); // true:Animal[Symbol.hasInstance](obj) 被调用
- 大多数 class 没有
Symbol.hasInstance
。在这种情况下,标准的逻辑是:使用obj instanceOf Class
检查Class.prototype
是否等于obj
的原型链中的原型之一。
换句话说就是,一个接一个地比较:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
obj.__proto__ === Class.prototype? obj.__proto__.__proto__ === Class.prototype? obj.__proto__.__proto__.__proto__ === Class.prototype? ... // 如果任意一个的答案为 true,则返回 true // 否则,如果我们已经检查到了原型链的尾端,则返回 false 在上面那个例子中,`rabbit.__proto__ === Rabbit.prototype`,所以立即就给出了结果。 而在继承的例子中,匹配将在第二步进行: class Animal {} class Rabbit extends Animal {} let rabbit = new Rabbit(); alert(rabbit instanceof Animal); // true // rabbit.__proto__ === Animal.prototype(无匹配) // rabbit.__proto__.__proto__ === Animal.prototype(匹配!)
这里还要提到一个方法 objA.isPrototypeOf(objB),如果 objA
处在 objB
的原型链中,则返回 true
。所以,可以将 obj instanceof Class
检查改为 Class.prototype.isPrototypeOf(obj)
。
但是 Class
的 constructor 自身是不参与检查的!检查过程只和原型链以及 Class.prototype
有关。
创建对象后,如果更改 prototype
属性,可能会导致有趣的结果。
就像这样:
1 2 3 4 5 6 7 8
function Rabbit() {} let rabbit = new Rabbit(); // 修改了 prototype Rabbit.prototype = {}; // ...再也不是 rabbit 了! alert( rabbit instanceof Rabbit ); // false
Object.prototype.toString.call
内建的 toString
方法可以被从对象中提取出来,并在任何其他值的上下文中执行。其结果取决于该值。
- 对于 number 类型,结果是
[object Number]
- 对于 boolean 类型,结果是
[object Boolean]
- 对于
null
:[object Null]
- 对于
undefined
:[object Undefined]
- 对于数组:
[object Array]
- ……等(可自定义)
1 2 3 4 5
let s = Object.prototype.toString; alert( s.call(123) ); // [object Number] alert( s.call(null) ); // [object Null] alert( s.call(alert) ); // [object Function]
Symbol.toStringTag
可以使用特殊的对象属性 Symbol.toStringTag
自定义对象的 toString
方法的行为。
例如:
1 2 3 4 5
let user = { [Symbol.toStringTag]: "User" }; alert( {}.toString.call(user) ); // [object User]