原型链
__proto__
__proto__
是[[Prototype]]
的因历史原因而留下来的 getter/setter。
请注意,__proto__
与内部的[[Prototype]]
不一样。__proto__
是[[Prototype]]
的 getter/setter。稍后,我们将看到在什么情况下理解它们很重要,在建立对 JavaScript 语言的理解时,让我们牢记这一点。
__proto__
属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数Object.getPrototypeOf/Object.setPrototypeOf
来取代__proto__
去 get/set 原型。 根据规范,__proto__
必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。
1let animal = {
2 eats: true,
3 walk() {
4 alert("Animal walk");
5 }
6};
7
8let rabbit = {
9 jumps: true,
10 __proto__: animal
11};
12
13let longEar = {
14 earLength: 10,
15 __proto__: rabbit
16};
17
18// walk 是通过原型链获得的
19longEar.walk(); // Animal walk
20alert(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
1let obj = {};
2
3alert(obj.__proto__ === Object.prototype); // true
1let arr = [1, 2, 3];
2
3// 它继承自 Array.prototype?
4alert( arr.__proto__ === Array.prototype ); // true
5
6// 接下来继承自 Object.prototype?
7alert( arr.__proto__.__proto__ === Object.prototype ); // true
8
9// 原型链的顶端为 null。
10alert( arr.__proto__.__proto__.__proto__ ); // null
1function f() {}
2
3alert(f.__proto__ == Function.prototype); // true
4alert(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
1let obj = {};
2
3let key = prompt("What's the key?", "__proto__");
4obj[key] = "some value";
5
6alert(obj[key]); // [object Object],并不是 "some value"!
1let obj = {};
2
3let key = prompt("What's the key?", "__proto__");
4obj[key] = new String("some value");
5
6alert(obj[key]); // 这里却会是 "some value"!
__proto__
属性很特殊:它必须是一个对象或者 null
。字符串不能成为原型。这就是为什么将字符串赋值给 __proto__
会被忽略。
创建无原型对象
Object.create(null)
或 {__proto__: null}
创建的无原型的对象。
对象会从 Object.prototype
继承内建的方法和 __proto__
getter/setter,会占用相应的键,且可能会导致副作用。原型为 null
时,对象才真正是空的。
[[HomeObject]]
当一个函数被定义为类或者对象方法时,它的 [[HomeObject]]
属性就成为了该对象。
然后 super
使用它来解析(resolve)父原型及其方法。
1let animal = {
2 name: "Animal",
3 eat() { // animal.eat.[[HomeObject]] == animal
4 alert(`${this.name} eats.`);
5 }
6};
7
8let rabbit = {
9 __proto__: animal,
10 name: "Rabbit",
11 eat() { // rabbit.eat.[[HomeObject]] == rabbit
12 super.eat();
13 }
14};
15
16let longEar = {
17 __proto__: rabbit,
18 name: "Long Ear",
19 eat() { // longEar.eat.[[HomeObject]] == longEar
20 super.eat();
21 }
22};
23
24// 正确执行
25longEar.eat(); // Long Ear eats.
应该是方法而不是函数属性
[[HomeObject]]
是为类和普通对象中的方法定义的。但是对于对象而言,方法必须确切指定为 method()
,而不是 "method: function()"
。
这个差别对我们来说可能不重要,但是对 JavaScript 来说却非常重要。
在下面的例子中,使用非方法(non-method)语法进行了比较。未设置 [[HomeObject]]
属性,并且继承无效:
1let animal = {
2 eat: function() { // 这里是故意这样写的,而不是 eat() {...
3 // ...
4 }
5};
6
7let rabbit = {
8 __proto__: animal,
9 eat: function() {
10 super.eat();
11 }
12};
13
14rabbit.eat(); // 错误调用 super(因为这里没有 [[HomeObject]])
Symbol.species
以给类添加一个特殊的静态 getter Symbol.species
。如果存在,则应返回 JavaScript 在内部用来在 map
和 filter
等方法中创建新实体的 constructor
。
如果我们希望像 map
或 filter
这样的内建方法返回常规数组,我们可以在 Symbol.species
中返回 Array
,就像这样:
1class PowerArray extends Array {
2 isEmpty() {
3 return this.length === 0;
4 }
5
6 // 内建方法将使用这个作为 constructor
7 static get [Symbol.species]() {
8 return Array;
9 }
10}
11
12let arr = new PowerArray(1, 2, 5, 10, 50);
13alert(arr.isEmpty()); // false
14
15// filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组
16let filteredArr = arr.filter(item => item >= 10);
17
18// filteredArr 不是 PowerArray,而是 Array
19alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function
内建类没有静态方法继承
内建对象有它们自己的静态方法,例如 Object.keys
,Array.isArray
等。
如我们所知道的,原生的类互相扩展。例如,Array
扩展自 Object
。
Symbol.hasInstance
instanceof 原理,obj instanceof Class
算法的执行过程大致如下:
- 如果这儿有静态方法
Symbol.hasInstance
,那就直接调用这个方法:
例如:1// 设置 instanceof 检查 2// 并假设具有 canEat 属性的都是 animal 3class Animal { 4 static [Symbol.hasInstance](obj) { 5 if (obj.canEat) return true; 6 } 7} 8let obj = { canEat: true }; 9alert(obj instanceof Animal); // true:Animal[Symbol.hasInstance](obj) 被调用
- 大多数 class 没有
Symbol.hasInstance
。在这种情况下,标准的逻辑是:使用obj instanceof Class
检查Class.prototype
是否等于obj
的原型链中的原型之一,即只需要判断, 实例对象__proto__
指针是否指向类的原型
换句话说就是,一个接一个地比较:1obj.__proto__ === Class.prototype? 2obj.__proto__.__proto__ === Class.prototype? 3obj.__proto__.__proto__.__proto__ === Class.prototype? 4... 5// 如果任意一个的答案为 true,则返回 true 6// 否则,如果我们已经检查到了原型链的尾端,则返回 false 7 8在上面那个例子中,`rabbit.__proto__ === Rabbit.prototype`,所以立即就给出了结果。 9 10而在继承的例子中,匹配将在第二步进行: 11 12class Animal {} 13class Rabbit extends Animal {} 14 15let rabbit = new Rabbit(); 16alert(rabbit instanceof Animal); // true 17 18// rabbit.__proto__ === Animal.prototype(无匹配) 19// rabbit.__proto__.__proto__ === Animal.prototype(匹配!)
这里还要提到一个方法 objA.isPrototypeOf(objB),如果 objA
处在 objB
的原型链中,则返回 true
。所以,可以将 obj instanceof Class
检查改为 Class.prototype.isPrototypeOf(obj)
。
但是 Class
的 constructor 自身是不参与检查的!检查过程只和原型链以及 Class.prototype
有关。
创建对象后,如果更改 prototype
属性,可能会导致有趣的结果。
就像这样:
1function Rabbit() {}
2let rabbit = new Rabbit();
3
4// 修改了 prototype
5Rabbit.prototype = {};
6
7// ...再也不是 rabbit 了!
8alert( rabbit instanceof Rabbit ); // false
Object.prototype.toString.call
内建的 toString
方法可以被从对象中提取出来,并在任何其他值的上下文中执行。其结果取决于该值。
- 对于 number 类型,结果是
[object Number]
- 对于 boolean 类型,结果是
[object Boolean]
- 对于
null
:[object Null]
- 对于
undefined
:[object Undefined]
- 对于数组:
[object Array]
- ……等(可自定义)
1let s = Object.prototype.toString;
2
3alert( s.call(123) ); // [object Number]
4alert( s.call(null) ); // [object Null]
5alert( s.call(alert) ); // [object Function]
Symbol.toStringTag
可以使用特殊的对象属性 Symbol.toStringTag
自定义对象的 toString
方法的行为。
例如:
1let user = {
2 [Symbol.toStringTag]: "User"
3};
4
5alert( {}.toString.call(user) ); // [object User]
Object.create()
Object.create() 可能出现的潜在的问题
令你使用的对象不继承 Object.prototype
原型的方法也可以防止原型污染攻击。如果恶意脚本向 Object.prototype
添加了一个属性,这个属性将能够被程序中的每一个对象所访问,而以 null 为原型的对象则不受影响。
1const user = {};
2
3// A malicious script:
4Object.prototype.authenticated = true;
5
6// Unexpectedly allowing unauthenticated user to pass through
7if (user.authenticated) {
8 // access confidential data...
9}
10Copy to Clipboard
用 Object.create() 实现类式继承
1// Shape - superclass
2function Shape() {
3 this.x = 0;
4 this.y = 0;
5}
6
7// superclass method
8Shape.prototype.move = function(x, y) {
9 this.x += x;
10 this.y += y;
11 console.info('Shape moved.');
12};
13
14// Rectangle - subclass
15function Rectangle() {
16 Shape.call(this); // call super constructor.
17}
18
19// subclass extends superclass
20Rectangle.prototype = Object.create(Shape.prototype);
21
22//If you don't set Rectangle.prototype.constructor to Rectangle,
23//it will take the prototype.constructor of Shape (parent).
24//To avoid that, we set the prototype.constructor to Rectangle (child).
25Rectangle.prototype.constructor = Rectangle;
26
27const rect = new Rectangle();
28
29console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true
30console.log('Is rect an instance of Shape?', rect instanceof Shape); // true
31rect.move(1, 1); // Outputs, 'Shape moved.'
Object.create() 的第二个参数
propertiesObject: 如果该参数被指定且不为 undefined
,则该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。这些属性对应于 Object.defineProperties()
的第二个参数。
1let o;
2
3// create an object with null as prototype
4o = Object.create(null);
5
6o = {};
7// is equivalent to:
8o = Object.create(Object.prototype);
9
10// Example where we create an object with a couple of
11// sample properties. (Note that the second parameter
12// maps keys to *property descriptors*.)
13o = Object.create(Object.prototype, {
14 // foo is a regular 'value property'
15 foo: {
16 writable: true,
17 configurable: true,
18 value: 'hello'
19 },
20 // bar is a getter-and-setter (accessor) property
21 bar: {
22 configurable: false,
23 get: function() { return 10; },
24 set: function(value) {
25 console.log('Setting `o.bar` to', value);
26 }
27/* with ES2015 Accessors our code can look like this
28 get() { return 10; },
29 set(value) {
30 console.log('Setting `o.bar` to', value);
31 } */
32 }
33});
34
35function Constructor() {}
36o = new Constructor();
37// is equivalent to:
38o = Object.create(Constructor.prototype);
39// Of course, if there is actual initialization code
40// in the Constructor function,
41// the Object.create() cannot reflect it
42
43// Create a new object whose prototype is a new, empty
44// object and add a single property 'p', with value 42.
45o = Object.create({}, { p: { value: 42 } });
46
47// by default properties ARE NOT writable,
48// enumerable or configurable:
49o.p = 24;
50o.p;
51// 42
52
53o.q = 12;
54for (const prop in o) {
55 console.log(prop);
56}
57// 'q'
58
59delete o.p;
60// false
61
62// to specify an ES3 property
63o2 = Object.create({}, {
64 p: {
65 value: 42,
66 writable: true,
67 enumerable: true,
68 configurable: true
69 }
70});
71/* is not equivalent to:
72This will create an object with prototype : {p: 42 }
73o2 = Object.create({p: 42}) */