原型链

__proto__

__proto__[[Prototype]] 的因历史原因而留下来的 getter/setter。
请注意,__proto__ 与内部的 [[Prototype]] 不一样__proto__[[Prototype]] 的 getter/setter。稍后,我们将看到在什么情况下理解它们很重要,在建立对 JavaScript 语言的理解时,让我们牢记这一点。
__proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型。 根据规范,__proto__ 必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。

js
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 中查找。

这里只有两个限制:

  1. 引用不能形成闭环。如果我们试图在一个闭环中分配 __proto__,JavaScript 会抛出错误。
  2. __proto__ 的值可以是对象,也可以是 null。而其他的类型都会被忽略。

几乎所有其他键/值获取方法都忽略继承的属性.
几乎所有其他键/值获取方法,例如 Object.keysObject.values 等,都会忽略继承的属性。
它们只会对对象自身进行操作。不考虑 继承自原型的属性。

  • 在 JavaScript 中,所有的对象都有一个隐藏的 [[Prototype]] 属性,它要么是另一个对象,要么就是 null
  • 我们可以使用 obj.__proto__ 访问它(历史遗留下来的 getter/setter,这儿还有其他方法,很快我们就会讲到)。
  • 通过 [[Prototype]] 引用的对象被称为“原型”。
  • 如果我们想要读取 obj 的一个属性或者调用一个方法,并且它不存在,那么 JavaScript 就会尝试在原型中查找它。
  • 写/删除操作直接在对象上进行,它们不使用原型(假设它是数据属性,不是 setter)。
  • 如果我们调用 obj.method(),而且 method 是从原型中获取的,this 仍然会引用 obj。因此,方法始终与当前对象一起使用,即使方法是继承的。
  • for..in 循环在其自身和继承的属性上进行迭代。所有其他的键/值获取方法仅对对象本身起作用。

prototype

js
1let obj = {};
2
3alert(obj.__proto__ === Object.prototype); // true
js
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
js
1function f() {}
2
3alert(f.__proto__ == Function.prototype); // true
4alert(f.__proto__.__proto__ == Object.prototype); // true,继承自 Object

基本数据类型

最复杂的事情发生在字符串、数字和布尔值上。
正如我们记忆中的那样,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 StringNumberBoolean 被创建。它们提供给我们操作字符串、数字和布尔值的方法然后消失。
这些对象对我们来说是无形地创建出来的。大多数引擎都会对其进行优化,但是规范中描述的就是通过这种方式。这些对象的方法也驻留在它们的 prototype 中,可以通过 String.prototypeNumber.prototypeBoolean.prototype 进行获取。

特殊值 nullundefined 比较特殊。它们没有对象包装器,所以它们没有方法和属性。并且它们也没有相应的原型。

Object.setPrototypeOf and Object.getPrototypeOf

使用 obj.__proto__ 设置或读取原型被认为已经过时且不推荐使用(deprecated)了(已经被移至 JavaScript 规范的附录 B,意味着仅适用于浏览器)。

现代的获取/设置原型的方法有:

原型历史

  • 构造函数的 "prototype" 属性自古以来就起作用。这是使用给定原型创建对象的最古老的方式。
  • 之后,在 2012 年,Object.create 出现在标准中。它提供了使用给定原型创建对象的能力。但没有提供 get/set 它的能力。一些浏览器实现了非标准的 __proto__ 访问器,以为开发者提供更多的灵活性。
  • 之后,在 2015 年,Object.setPrototypeOfObject.getPrototypeOf 被加入到标准中,执行与 __proto__ 相同的功能。由于 __proto__ 实际上已经在所有地方都得到了实现,但它已过时,所以被加入到该标准的附件 B 中,即:在非浏览器环境下,它的支持是可选的。
  • 之后,在 2022 年,官方允许在对象字面量 {...} 中使用 __proto__(从附录 B 中移出来了),但不能用作 getter/setter obj.__proto__(仍在附录 B 中)。

如果速度很重要,就请不要修改已存在的对象的 [[Prototype]]
从技术上来讲,我们可以在任何时候 get/set [[Prototype]]。但是通常我们只在创建对象的时候设置它一次,自那之后不再修改:rabbit 继承自 animal,之后不再更改。
并且,JavaScript 引擎对此进行了高度优化。用 Object.setPrototypeOf 或 obj.__proto__= “即时”更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化。因此,除非你知道自己在做什么,或者 JavaScript 的执行速度对你来说完全不重要,否则请避免使用它。

原型只能是对象或者 null

js
1let obj = {};
2
3let key = prompt("What's the key?", "__proto__");
4obj[key] = "some value";
5
6alert(obj[key]); // [object Object],并不是 "some value"!
js
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)父原型及其方法。

js
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]] 属性,并且继承无效:

js
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 在内部用来在 mapfilter 等方法中创建新实体的 constructor
如果我们希望像 mapfilter 这样的内建方法返回常规数组,我们可以在 Symbol.species 中返回 Array,就像这样:

js
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.keysArray.isArray 等。
如我们所知道的,原生的类互相扩展。例如,Array 扩展自 Object

Symbol.hasInstance

instanceof 原理,obj instanceof Class 算法的执行过程大致如下:

  1. 如果这儿有静态方法 Symbol.hasInstance,那就直接调用这个方法:
    例如:
    js
    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) 被调用
  2. 大多数 class 没有 Symbol.hasInstance。在这种情况下,标准的逻辑是:使用 obj instanceof Class 检查 Class.prototype 是否等于 obj 的原型链中的原型之一,即只需要判断, 实例对象 __proto__ 指针是否指向 类的原型 换句话说就是,一个接一个地比较:
    js
    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 属性,可能会导致有趣的结果。
就像这样:

js
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]
  • ……等(可自定义)
js
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 方法的行为。
例如:

js
1let user = {
2  [Symbol.toStringTag]: "User"
3};
4
5alert( {}.toString.call(user) ); // [object User]

Object.create()

Object.create() 可能出现的潜在的问题

令你使用的对象不继承 Object.prototype 原型的方法也可以防止原型污染攻击。如果恶意脚本向 Object.prototype 添加了一个属性,这个属性将能够被程序中的每一个对象所访问,而以 null 为原型的对象则不受影响。

js
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() 实现类式继承

js
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() 的第二个参数。

js
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}) */