使用 __proto__
操作原型的方式已经是过时了。规范规定此属性在浏览器环境中提供即可,其他环境(比如 Node.js )是可选实现。
我们可以使用下面更加现代的方式,作为 __proto__
的替代使用:
Object.create(proto[, descriptors])
:使用给定的proto
作为新创建空对象内部的[[Prototype]]
属性值。descriptors
是可选的、附加到新对象上面的属性描述符。Object.getPrototypeOf(obj)
:返回obj
的[[Prototype]]
值。Object.setPrototypeOf(obj, proto)
:将obj
的[[Prototype]]
设置为proto
。
举个例子:
let animal = {
eats: true
};
// 创建一个对象,原型设置为 animal
let rabbit = Object.create(animal);
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf(rabbit, {}); // 修改 rabbit 的原型为 {}
Object.create
还接收可选的第二个参数:属性描述符。为新创建对象添加额外属性:
let animal = {
eats: true
};
let rabbit = Object.create(animal, {
jumps: {
value: true
}
});
alert(rabbit.jumps); // true
属性描述符在《属性标记和描述符》一章里已做了介绍。
我们还能使用 Object.create
方法来克隆对象,比使用 for..in
方式复制更加强大:
// 完全相同的 obj 的浅克隆
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
clone
是 obj
的真实副本,包括所有属性:可枚举不可枚举的,数据属性访问器属性,还有一样的 [[Prototype]]
。
原型简史
如果梳理一遍管理 [[Prototype]]
的方法,发现还挺多的。这许多的方法却做着一样的事情。
为什么呢?
这归咎于历史原因。
构造函数的“
prototype
”属性从很早以前就有了。2012 年后,标准引入了
Object.create
方法。允许在创建对象时,指定原型对象,但没有提供获取/设置圆形的方法。因此浏览器厂商实现了非标准的__proto__
访问器属性,来获取/设置原型。2015 年后,标准又加入了
Object.setPrototypeOf
和Object.getPrototypeOf
方法,功能上跟__proto__
属性是一样的。但此时,__proto__
属性已经成为事实标准了。因此它被写进了标准的附录 B :对非浏览器环境而言,这个属性是可选实现。
这是到目前为止,我们能够采用的方法。
为什么要用 getPrototypeOf
/setPrototypeOf
方法去替代 __proto__
属性呢?这个问题很有趣,我们要理解为什么使用 __proto__
属性会坏的。继续读下去你就知道了。
💡 如果有性能要求,请不要对已有对象的 [[Prototype]] 做修改 ** 从技术上将,我们可以随意获取/设置
[[Prototype]]
。但我们基本上创建完对象后,就不再修改对象原型了。举个例子:rabbit
继承自animal
,那么这个继承关系,后面就不再修改了。为什么呢?
因为,在 JavaScript 引擎内部,对原型操作做了高度优化。使用Object.setPrototypeOf
或obj.__proto__
的方式修改对象原型,会破坏内部的优化,所以是是一个非常低效的操作。因此,除非你知道自己在做什么,否则尽量避免修改对象原型,当然如果你的项目没有性能考虑,可以这么做。
“至纯”对象
对象也可以用作关联数组来存储键/值对数据。
但是如果我们使用用户输入的内容作为对象的 key 存入的话,就可能会发生问题:如果输入的是“proto”就有问题,任何其他的值倒是不碍事。
看个例子:
let obj = {};
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // [object Object], 不是 "some value"!
如果这里用户输入了“proto”,这个赋值就会被忽略!
对我们来说,应该不是一件值得惊讶的事情。这是因为 proto 有点特殊:它的值只可能是对象或者 null。字符串是无法作为原型的。
但这个行为可不是我们想要的,对吧?我们才不管为什么“proto”没有正确存储呢,这是个 bug!
上面举得这个例子,基本不是什么太严重的问题。但如果赋值的确实是个对象的话,原型可就被我们改了啊。这样的话,执行结果就可能是完全意料不到的了。
更坏的是——往往开发者可能意识不到这个问题。这样的 bug 就很难引起注意,甚至成为系统的潜在漏洞了,特别是在用 JavaScript 书写服务端代码时。
当然,不只是“proto”,如果我们存储的是“toString”这些能覆盖从原型对象中继承的方法名的话,可能也会有问题。
var obj = {}
obj.toString = 'foo'
// 因为 toString 方法被我们覆盖了,alert 时调用 obj.toString 失败,就报错了
alert(obj) // Error: Cannot convert object to primitive value
那么如何避免这类问题发生呢?
首先,我们可以改用 Map
,这就没问题了。
但是,使用对象用起来也没啥问题。关键怎么处理上述讲的一些特殊、会的引发问题。当然,语言创始人很久以前就已经想过这个问题了。
__proto__
不是对象属性,而是 Object.prototype
的访问器属性:
所以,读取或设置 obj.__proto__
,就是调用原型对象上对应的 getter/setter 来获取/设置 [[Prototype]]
。
正如开头所说:__proto__
只是一种访问 [[Prototype]]
的方式,并非 [[Prototype]]
属性本身。
现在,如果想把对象作为关联数组使用,我们可以使用一个小技巧:
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
):
这样,我们也就没有了继承而来的 __proto__
的 getter/setter 了。现在它终于能作为一个普通的数据属性处理了。因此,上面的例子能够运行正确。
我们可以称这种对象是“至纯”或“纯字典”对象,因为它比用 {...}
生成的普通对象还有简单。
当然也有缺点:这些对象没有任何的内置方法。比如:toString
。
let obj = Object.create(null);
alert(obj); // 出错 (没有 toString 方法)
但对关联数组来说,没啥问题(因为也不用)。
需要注意的是,大多数与对象相关的方法都是以 “Object.something()
”的形式出现的,像 Object.keys(obj)
——它们没有定义在原型中,所以对至纯对象来说,这些方法依旧能起作用:
let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";
alert(Object.keys(chineseDictionary)); // hello, bye
总结
现代的访问和设置原型的方法包括:
Object.create(proto[, descriptors])
:使用给定的proto
作为新创建空对象内部的[[Prototype]]
属性值(可以为null
)。descriptors
是可选的、附加到新对象上面的属性描述符。Object.getPrototypeOf(obj)
:返回obj
的[[Prototype]]
值(与__proto__
getter 作用一样)。Object.setPrototypeOf(obj, proto)
:将obj
的[[Prototype]]
设置为proto
(与__proto__
setter 作用一样)。
如果我们想使用用户输入的内容作为对象属性名,那么内置的 __proto__
getter/setter 是不安全的。如果用户输入的是“proto”,那么就会有问题。
因此,我们可以使用 Object.create(null)
创建没有 __proto__
的“至纯”对象,或者直接使用 Map
。
同时,Object.create
也提供了一种浅克隆对象的简单方法,而且这种克隆是准确复制每个属性描述符的。
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
我们也搞懂了 __proto__
就是建立在 [[Prototype]]
属性上的 getter/setter,存在于 Object.prototype
对象上。
我们还能使用 Object.create(null)
创建一个没有原型的对象。这样的对象被用作“纯净字典”,它是没有“__proto__
”键名问题的。
其他方法还有:
Object.keys(obj)
/Object.values(obj)
/Object.entries(obj)
:返回由可枚举、自有的字符串属性的属性名/属性值/属性键值对组成的数组。Object.getOwnPropertySymbols(obj)
:返回自有的所有由 Symbol 类型属性名 组成的数组。Object.getOwnPropertyNames(obj)
:返回自有的所有由 字符串类型属性名 组成的数组。Reflect.ownKeys(obj)
:返回由所有自有属性名组成的数组。obj.hasOwnProperty(key)
:如果obj
有key
这个属性(不是继承的),就返回true
,否则返回false
。
所有的方法都返回对象的自身属性(像 Object.keys
和其他方法做的那样)。如果需要获取继承属性,就要使用 for..in
循环了。
(完)
📄 文档信息
🕘 更新时间:2020/01/16
🔗 原文链接:http://javascript.info/prototype-methods