使用 __proto__ 操作原型的方式已经是过时了。规范规定此属性在浏览器环境中提供即可,其他环境(比如 Node.js )是可选实现。

我们可以使用下面更加现代的方式,作为 __proto__ 的替代使用:

举个例子:

  1. let animal = {
  2. eats: true
  3. };
  4. // 创建一个对象,原型设置为 animal
  5. let rabbit = Object.create(animal);
  6. alert(rabbit.eats); // true
  7. alert(Object.getPrototypeOf(rabbit) === animal); // true
  8. Object.setPrototypeOf(rabbit, {}); // 修改 rabbit 的原型为 {}

Object.create 还接收可选的第二个参数:属性描述符。为新创建对象添加额外属性:

  1. let animal = {
  2. eats: true
  3. };
  4. let rabbit = Object.create(animal, {
  5. jumps: {
  6. value: true
  7. }
  8. });
  9. alert(rabbit.jumps); // true

属性描述符在《属性标记和描述符》一章里已做了介绍。

我们还能使用 Object.create 方法来克隆对象,比使用 for..in 方式复制更加强大:

  1. // 完全相同的 obj 的浅克隆
  2. let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

cloneobj 的真实副本,包括所有属性:可枚举不可枚举的,数据属性访问器属性,还有一样的 [[Prototype]]

原型简史

如果梳理一遍管理 [[Prototype]] 的方法,发现还挺多的。这许多的方法却做着一样的事情。

为什么呢?

这归咎于历史原因。

  • 构造函数的“prototype”属性从很早以前就有了。

  • 2012 年后,标准引入了 Object.create 方法。允许在创建对象时,指定原型对象,但没有提供获取/设置圆形的方法。因此浏览器厂商实现了非标准的 __proto__ 访问器属性,来获取/设置原型。

  • 2015 年后,标准又加入了 Object.setPrototypeOfObject.getPrototypeOf 方法,功能上跟 __proto__ 属性是一样的。但此时,__proto__ 属性已经成为事实标准了。因此它被写进了标准的附录 B :对非浏览器环境而言,这个属性是可选实现。

这是到目前为止,我们能够采用的方法。

为什么要用 getPrototypeOf/setPrototypeOf 方法去替代 __proto__ 属性呢?这个问题很有趣,我们要理解为什么使用 __proto__ 属性会坏的。继续读下去你就知道了。

💡 如果有性能要求,请不要对已有对象的 [[Prototype]] 做修改 ** 从技术上将,我们可以随意获取/设置 [[Prototype]]。但我们基本上创建完对象后,就不再修改对象原型了。举个例子:rabbit 继承自 animal,那么这个继承关系,后面就不再修改了。为什么呢?
因为,在 JavaScript 引擎内部,对原型操作做了高度优化。使用 Object.setPrototypeOfobj.__proto__ 的方式修改对象原型,会破坏内部的优化,所以是是一个非常低效的操作。因此,除非你知道自己在做什么,否则尽量避免修改对象原型,当然如果你的项目没有性能考虑,可以这么做。

“至纯”对象

对象也可以用作关联数组来存储键/值对数据。

但是如果我们使用用户输入的内容作为对象的 key 存入的话,就可能会发生问题:如果输入的是“proto”就有问题,任何其他的值倒是不碍事。

看个例子:

  1. let obj = {};
  2. let key = prompt("What's the key?", "__proto__");
  3. obj[key] = "some value";
  4. alert(obj[key]); // [object Object], 不是 "some value"!

如果这里用户输入了“proto”,这个赋值就会被忽略!

对我们来说,应该不是一件值得惊讶的事情。这是因为 proto 有点特殊:它的值只可能是对象或者 null。字符串是无法作为原型的。

但这个行为可不是我们想要的,对吧?我们才不管为什么“proto”没有正确存储呢,这是个 bug!

上面举得这个例子,基本不是什么太严重的问题。但如果赋值的确实是个对象的话,原型可就被我们改了啊。这样的话,执行结果就可能是完全意料不到的了。

更坏的是——往往开发者可能意识不到这个问题。这样的 bug 就很难引起注意,甚至成为系统的潜在漏洞了,特别是在用 JavaScript 书写服务端代码时。

当然,不只是“proto”,如果我们存储的是“toString”这些能覆盖从原型对象中继承的方法名的话,可能也会有问题。

  1. var obj = {}
  2. obj.toString = 'foo'
  3. // 因为 toString 方法被我们覆盖了,alert 时调用 obj.toString 失败,就报错了
  4. alert(obj) // Error: Cannot convert object to primitive value

那么如何避免这类问题发生呢?

首先,我们可以改用 Map,这就没问题了。

但是,使用对象用起来也没啥问题。关键怎么处理上述讲的一些特殊、会的引发问题。当然,语言创始人很久以前就已经想过这个问题了。

__proto__ 不是对象属性,而是 Object.prototype 的访问器属性:

原型方法,没有 __proto__ 的对象 - 图1

所以,读取或设置 obj.__proto__,就是调用原型对象上对应的 getter/setter 来获取/设置 [[Prototype]]

正如开头所说:__proto__ 只是一种访问 [[Prototype]] 的方式,并非 [[Prototype]] 属性本身。

现在,如果想把对象作为关联数组使用,我们可以使用一个小技巧:

  1. let obj = Object.create(null);
  2. let key = prompt("What's the key?", "__proto__");
  3. obj[key] = "some value";
  4. alert(obj[key]); // "some value"

Object.create(null) 创建了一个没有原型的空对象([[Prototype]]null):

原型方法,没有 __proto__ 的对象 - 图2

这样,我们也就没有了继承而来的 __proto__ 的 getter/setter 了。现在它终于能作为一个普通的数据属性处理了。因此,上面的例子能够运行正确。

我们可以称这种对象是“至纯”或“纯字典”对象,因为它比用 {...} 生成的普通对象还有简单。

当然也有缺点:这些对象没有任何的内置方法。比如:toString

  1. let obj = Object.create(null);
  2. alert(obj); // 出错 (没有 toString 方法)

但对关联数组来说,没啥问题(因为也不用)。

需要注意的是,大多数与对象相关的方法都是以 “Object.something()”的形式出现的,像 Object.keys(obj)——它们没有定义在原型中,所以对至纯对象来说,这些方法依旧能起作用:

  1. let chineseDictionary = Object.create(null);
  2. chineseDictionary.hello = "你好";
  3. chineseDictionary.bye = "再见";
  4. alert(Object.keys(chineseDictionary)); // hello, bye

总结

现代的访问和设置原型的方法包括:

如果我们想使用用户输入的内容作为对象属性名,那么内置的 __proto__ getter/setter 是不安全的。如果用户输入的是“proto”,那么就会有问题。

因此,我们可以使用 Object.create(null) 创建没有 __proto__ 的“至纯”对象,或者直接使用 Map

同时,Object.create 也提供了一种浅克隆对象的简单方法,而且这种克隆是准确复制每个属性描述符的。

  1. let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

我们也搞懂了 __proto__ 就是建立在 [[Prototype]] 属性上的 getter/setter,存在于 Object.prototype 对象上。

我们还能使用 Object.create(null) 创建一个没有原型的对象。这样的对象被用作“纯净字典”,它是没有“__proto__”键名问题的。

其他方法还有:

所有的方法都返回对象的自身属性(像 Object.keys 和其他方法做的那样)。如果需要获取继承属性,就要使用 for..in 循环了。

(完)


📄 文档信息

🕘 更新时间:2020/01/16
🔗 原文链接:http://javascript.info/prototype-methods