[译]JavaScript: __proto__
原文:http://www.2ality.com/2012/10/proto.html
本文讲一下特殊属性__proto__,通过该属性可以获取或设置一个对象的原型.想要理解这篇文章,你必须已经熟悉了JavaScript的原型继承 [1].
1.特殊属性__proto__
ECMAScript标准中规定,一个对象的内部属性[[Prototype]]指向自己的原型.在ECMAScript 5中,该属性是不能被直接读取或修改的,但是可以通过Object.getPrototypeOf()间接的读取到它.还可以使用Object.create()创建一个拥有指定原型的新对象.例如,下面的代码创建了一个对象obj,它的原型是myProto> var myProto = {}; > var obj = Object.create(myProto); > Object.getPrototypeOf(obj) === myProto true
__proto__ (发音为“dunder proto”,dunder是“double underscore”的简拼)最初出现在Firefox中,作为内部属性[[Prototype]]的别名.使用__proto__的话,上面的代码可以改写成:
> var myProto = {}; > var obj = { __proto__: myProto }; > obj.__proto__ === myProto true
__proto__属性的值就是它的原型:
> obj.__proto__ === Object.getPrototypeOf(obj) true
自从__proto__出现在Firefox中以后,它就变的越来越流行,现在V8 (Chrome, Node.js)和Nitro (Safari)也已经支持了它.但ECMAScript 5并没有标准化__proto__,不过由于它现在的流行程度,它将会成为ECMAScript 6规范的一部分.
1.1 检测是否支持__proto__
如果JavaScript引擎实现了 __proto__ ,那么下面的表达式会返回true:Object.getPrototypeOf({ __proto__: null }) === null
1.2 对象作为Map使用
如果你需要把一个对象作为一个字符串到值的Map来使用的话(真正的Map可以有任意类型的键)[2],那么你必须做个判断,如果传入的键名是"__proto__",则将它转义为一个不和已有的属性'__proto__'冲突的键名[2].例如:function escapeKey(key) { // 我们需要转义"__proto__"成为"__proto__%",还得转义"__proto__%"成为"__proto__%%",依次类推.
if (key.indexOf("__proto__") === 0) { return key+"%"; } else { return key; } }
2.两个用处
__proto__两个最大的用处是:创建一个以指定对象为原型的对象,以及,为内置类型添加子类型.
使用Object.create()来替代的话,你有两种写法,但都不是很简洁:
2.1 创建一个以指定对象为原型的对象
如果你要创建一个非空的对象,则使用__proto__的优点会比较明显:var obj = { __proto__: myProto, foo: 123, bar: "abc" };
// 替代方法1:创建一个空对象,然后赋值 var obj = Object.create(myProto); obj.foo = 123; obj.bar = "abc"; // 替代方法2:属性描述符 var obj = Object.create(myProto, { foo: { value: 123, writable: true, enumerable: true, configurable: true }, bar: { value: "abc", writable: true, enumerable: true, configurable: true } });
2.2 为内置类型添加子类型
总所周知,在JavaScript中很难为内置类型添加子类型[3],这有两个原因:第一个是,JavaScript中,标准的子类型化模式是,在子类的构造函数中,将子类型的实例this通过call方法传入到超类型的构造函数中,这样子类型的实例就能拥有超类型的属性.但是,大部分内置类型的构造函数都不会允许你那样做,它们会忽略掉传入的this.第二个原因是,一些内置类型的对象实例是很特殊的,即使你能成功的把子类型的对象实例传进超类型的构造函数中去,你也得不到按预期工作的对象实例.最常见的例子就是数组,如果数组中新添加了一个元素,它的length属性会自动更新,这个特性子类型无法继承下来.
译者注:为自定义类型添加子类型的通用方法是:function Super(x, y) {
this.x = x;
this.y = y;
}
function Sub(x, y, z) {
Super.call(this, x, y); //为子类型的对象实例添加超类型的属性,子类型的对象实例作为超类型的构造函数中的this值 this.z = z; //添加子类型的扩展属性
}但如果是内置类型的话,就不行了.
借助__proto__的话,可以这样实现数组的子类型:
var MyArrayProto = Object.create(Array.prototype); //还可以写成var MyArrayProto = {__proto__:Array.prototype}; MyArrayProto.foo = function (...) { ... }; function createMyArray() { var arr = Array.prototype.slice.call(arguments); arr.__proto__ = MyArrayProto; return arr; } var myarr = createMyArray(1,2,3); //myarr会有foo方法,也会有其他的数组方法
3.ECMAScript 6
正如前面所提到的,ECMAScript 6将会添加对__proto__的支持.目前可以确定的功能有,你可以用它来获取到一个对象的原型,还可以用在对象字面量中设置这个对象的原型.对象字面量配合__proto__可以视为是一种更友好的Object.create()的替代者,对于大多数情况来说,足够好用了.
目前还不确定的功能是,能不能通过__proto__修改一个已存在对象的原型.目前最主要需要用到这个特性的地方就是给数组新增子类型,不过未来也许会有更好的方法来实现这个需求,比如通过函数Array.createArray(proto).
ECMAScript 6还可能会提供一种手段来关闭一些对象上的__proto__属性的访问,甚至可能是全部对象.
4.更多资料
- MDN: “__proto__”
- “The magic __proto__ property” by Asen Bozhilov