一、每一个JavaScript对象(除了null)都具有一个属性,叫proto,这个属性会指向该对象的原型

  1. function Person() {
  2. }
  3. var person = new Person();
  4. console.log(person.__proto__ === Person.prototype); // true

设置原型

一、

  1. let animal = {
  2. eats: true
  3. };
  4. let rabbit = {
  5. jumps: true
  6. };
  7. rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal

2、现在,如果我们从rabbit中读取一个它没有的属性,JavaScript 会自动从animal中获取。

  1. let animal = {
  2. eats: true
  3. };
  4. let rabbit = {
  5. jumps: true
  6. };
  7. rabbit.__proto__ = animal; // (*)
  8. // 现在这两个属性我们都能在 rabbit 中找到:
  9. alert( rabbit.eats ); // true // 将animal设置为rabbit的原型
  10. alert( rabbit.jumps ); // true

3、当alert试图读取rabbit.eats时,因为它不存在于rabbit中,所以 JavaScript 会顺着[[Prototype]]引用,在animal中查找(自下而上):
image.png
(1)在这儿我们可以说 “animal是rabbit的原型”,或者说 “rabbit的原型是从animal继承而来的”。
4、因此,如果animal有许多有用的属性和方法,那么它们将自动地变为在rabbit中可用。这种属性被称为“继承”。
5、如果我们在animal中有一个方法,它可以在rabbit中被调用:

  1. let animal = {
  2. eats: true,
  3. walk() {
  4. alert("Animal walk");
  5. }
  6. };
  7. let rabbit = {
  8. jumps: true,
  9. __proto__: animal
  10. };
  11. // walk 方法是从原型中获得的
  12. rabbit.walk(); // Animal walk

(1)该方法是自动地从原型中获得的,像这样:
image.png
6、原型链可以很长:

  1. let animal = {
  2. eats: true,
  3. walk() {
  4. alert("Animal walk");
  5. }
  6. };
  7. let rabbit = {
  8. jumps: true,
  9. __proto__: animal
  10. };
  11. let longEar = {
  12. earLength: 10,
  13. __proto__: rabbit
  14. };
  15. // walk 是通过原型链获得的
  16. longEar.walk(); // Animal walk
  17. alert(longEar.jumps); // true(从 rabbit)

(1)现在,如果我们从longEar中读取一些它不存在的内容,JavaScript 会先在rabbit中查找,然后在animal中查找。
(2)这里只有两个限制:
① 引用不能形成闭环。如果我们试图在一个闭环中分配proto,JavaScript 会抛出错误。
proto的值可以是对象,也可以是null。而其他的类型都会被忽略。
五、一个对象只能有一个[[Prototype]]。一个对象不能从其他两个对象获得继承。

不推荐使用proto

一、proto被认为是过时且不推荐使用的(deprecated),这里的不推荐使用是指 JavaScript 规范中规定,proto必须仅在浏览器环境下才能得到支持。
1、但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。
六、proto是[[Prototype]]的因历史原因而留下来的 getter/setter
1、proto与内部的[[Prototype]]不一样。proto是[[Prototype]]的 getter/setter。
2、proto属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数Object.getPrototypeOf/Object.setPrototypeOf来取代proto去 get/set 原型。

为什么proto不好

原型简史

一、如果我们数一下有多少种处理[[Prototype]]的方式,答案是有很多!很多种方法做的都是同一件事儿!
为什么会出现这种情况?
二、这是历史原因。

  • 构造函数的”prototype”属性自古以来就起作用。
  • 之后,在 2012 年,Object.create出现在标准中。它提供了使用给定原型创建对象的能力。但没有提供 get/set 它的能力。因此,许多浏览器厂商实现了非标准的proto访问器,该访问器允许用户随时 get/set 原型。
  • 之后,在 2015 年,Object.setPrototypeOf和Object.getPrototypeOf被加入到标准中,执行与proto相同的功能。由于proto实际上已经在所有地方都得到了实现,但它已过时,所以被加入到该标准的附件 B 中,即:在非浏览器环境下,它的支持是可选的。

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

proto会引起的bug

一、我们知道,对象可以用作关联数组(associative arrays)来存储键/值对。
二、但是如果我们尝试在其中存储用户提供的键(例如:一个用户输入的字典),我们可以发现一个有趣的小故障:所有的键都正常工作,除了”proto“。
【示例1】看一下这个例子:

let obj = {};

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // [object Object],并不是 "some value"!

1、这里如果用户输入proto,那么赋值会被忽略!
2、proto属性很特别:它必须是对象或者null。字符串不能成为 prototype。
(1)我们想要存储键值对,然而键名为”proto“的键值对没有被正确存储。所以这是一个 bug。
3、在这里,后果并没有很严重。但是在其他情况下,我们可能会对对象进行赋值操作,然后原型可能就被更改了。结果,可能会导致完全意想不到的结果。
4、最可怕的是 —— 通常开发者完全不会考虑到这一点。这让此类 bug 很难被发现,甚至变成漏洞,尤其是在 JavaScript 被用在服务端的时候。
【示例2】为默认情况下为函数的toString以及其他内建方法执行赋值操作,也会出现意想不到的结果。

避免这样的问题的方式

一、我们怎么避免这样的问题呢?

Map

一、首先,我们可以改用Map来代替普通对象进行存储,这样一切都迎刃而解。

Object

一、但是Object在这里同样可以运行得很好,因为 JavaScript 语言的制造者很早就注意到了这个问题。
二、proto不是一个对象的属性,只是Object.prototype的访问器属性:
image.png
1、因此,如果obj.proto被读取或者赋值,那么对应的 getter/setter 会被从它的原型中调用,它会 set/get[[Prototype]]。
(1)当使用obj.proto时,可以理解成返回了Object.getPrototypeOf(obj)
三、proto是一种访问[[Prototype]]的方式,而不是[[prototype]]本身。

Object.create(null) -> “very plain” objects

一、现在,我们想要将一个对象用作关联数组,并且摆脱此类问题,我们可以通过Object.create(null)来创建没有原型的对象:

let obj = Object.create(null);

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // "some value"

二、Object.create(null)创建了一个空对象,这个对象没有原型([[Prototype]]是null):
image.png
三、因此,它没有继承proto的 getter/setter 方法。现在,它被作为正常的数据属性进行处理,因此上面的这个示例能够正常工作。
四、我们可以把这样的对象称为 “very plain” 或 “pure dictionary” 对象,因为它们甚至比通常的普通对象(plain object){…}还要简单。
1、对于”very plain”对象而言,使用”proto“作为键是没有问题的。
五、缺点是这样的对象没有任何内建的对象的方法,例如toString:

let obj = Object.create(null);

alert(obj); // Error (no toString)

六、但是它们通常对关联数组而言还是很友好。
七、请注意,大多数与对象相关的方法都是Object.something(…),例如Object.keys(obj)—— 它们不在 prototype 中,因此在 “very plain” 对象中它们还是可以继续使用:

let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";

alert(Object.keys(chineseDictionary)); // hello,bye