[译]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__两个最大的用处是:创建一个以指定对象为原型的对象,以及,为内置类型添加子类型.

2.1 创建一个以指定对象为原型的对象

如果你要创建一个非空的对象,则使用__proto__的优点会比较明显:
var obj = {
    __proto__: myProto,
    foo: 123,
    bar: "abc"
};
使用Object.create()来替代的话,你有两种写法,但都不是很简洁:
// 替代方法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.更多资料

5.参考

  1. Prototypes as classes – an introduction to JavaScript inheritance
  2. The pitfalls of using objects as maps in JavaScript
  3. Subtyping JavaScript built-ins
posted @ 2012-10-05 20:18  紫云飞  阅读(7990)  评论(0编辑  收藏  举报