原文:http://www.2ality.com/2012/10/proto.html
本文講一下特殊屬性__proto__,通過該屬性可以獲取或設置一個對象的原型.想要理解這篇文章,你必須已經熟悉了JavaScript的原型繼承 [1].
1.特殊屬性__proto__
ECMAScript標准中規定,一個對象的內部屬性[ [Prototype]]指向自己的原型.在ECMAScript 5中,該屬性是不能被直接讀取或修改的,但是可以通過 Object.getPrototypeOf()間接的讀取到它.還可以使用 Object.create()創建一個擁有指定原型的新對象.例如,下面的代碼創建了一個對象o bj,它的原型是 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