如何創建一個JavaScript裸對象

所謂裸對象,即 naked object ,是指沒有原型(spec中以[[proto]]內建屬性表示)的對象。

JavaScript是少見的採用原型繼承的語言。訪問一個對象的屬性時,會首先看它自己的屬性,所謂 own property 是也,如果找不到,則在其原型中查找,再找不到就繼續找這個原型的原型,這就構成所謂的原型鏈。

原型繼承提供了一種很獨特的共享信息的方式,不過也帶來一些有趣的問題。比如with構造。

我在2011年w3ctech廣州的演講中提到過with構造的問題,所以在strict模式中with就被禁用了。

其中一個問題是,with(obj)時,obj如果是你[b]不可掌控[/b]的對象,會引入無法控制的風險。所謂不可掌控,例如瀏覽器對象(像在演講中舉的event對象),或者第三方庫的對象或可能被第三方庫修改的對象(例如DOM對象就是如此,許多庫會在上面加各種東西)。

這和原型有什麼關係?很有關係!因爲with不是僅僅查找own property,而是也會上溯原型鏈。

例子:

var name = 'hax'
with (console) {
log('Hello ' + name)
}


注意,許多實現裏 console.log 的 log 方法並非直接在 console 對象上,而是在 console 的原型上。

如此看上溯原型鏈似乎是件好事,但是考慮這個代碼:

var name = 'hax', count = 3
with (console) {
for (var i = 0; i < count; i++)
log('Hello ' + name)
}

這代碼在有些環境可以跑,如NodeJS;但在Chrome裏就不行了,會死循環(我沒高興實驗,歡迎小白鼠嘗試)。雖然大家用的都是V8引擎,但是Chrome裏的console上有一個count()方法。

更討厭的是,幾乎所有對象都有原型,一直到Object.prototype。考慮如下代碼:

function constructor() {
...
}
with ({}) {
var x = constructor()
}


實際結果等價於 x = Object() ,因爲 Object.prototype.constructor == Object 。

這也導致修改Object.prototype就可能干擾所有的 with 塊。而Object.prototype是所有對象的原型,換句話說,幾乎所有情況下都是[b]不可掌控[/b]的。

【注意,確實是存在不以 Object.prototype 爲原型的對象,比如某些環境裏的host對象,但是顯然它們也不屬於你可掌控的對象。】

可見with的最初設計就有失誤,如果是僅僅查找 own property 會好很多。不過現在已經不可能做這樣的修改,所以只能想其他方式,比如如果我們真正的有 naked object,那麼就可以做到對對象的完全掌控。


聽上去不太可能,除了null和undefined,所有對象(包括number、string和boolean通常會自動裝箱成包裝對象)都是Object的實例,也就是有Object.prototype作爲原型鏈的最末端。

不過ES5所增加的Object.create()方法就提供了這樣一個能力!只要調用Object.create(null)就可以得到一個裸對象!這這不錯。所以我們得到了一個相對安全的with方式如下:

with (Context({a: 1, b: 2})) {
console.log(a, b)
}

function Context(obj) {
var pds = {}
var names = Object.getOwnPropertyNames(obj)
for (var i = 0; i < names.length; i++) {
pds[names[i]] = Object.getOwnPropertyDescriptor(obj, names[i])
}
return Object.create(null, pds)
}


不過,對於老的,沒有Object.create方法的瀏覽器怎麼辦?

許多瀏覽器有 __proto__ 屬性可以直接訪問對象的 __proto__ 。所以NCZ寫過一篇關於裸對象的文章:[url=http://www.nczonline.net/blog/2008/07/10/naked-javascript-objects/]Naked JavaScript objects[/url]就提到這個方式。看上去其實比Object.create還要簡單:

with({a: 1, b: 2, __proto__: null}) { ... }


不過還有一些老瀏覽器不支持 __proto__ ,例如IE。怎麼辦呢?


其實有辦法。

可以發現,在整個JavaScript對象世界裏,似乎所有的對象都有原型,但是有一個例外。

沒錯,那就是所有對象之源:Object.prototype 。它自己是沒有原型的!
var root = Object.prototype
Object.getPrototypeOf(root) === null // true


如果我們把Object.prototype上的所有屬性都delete掉,我們就得到了一個裸對象!

不過這樣把Object.prototype給破壞,顯然會影響我們的程序。好在咱有 iframe !每個iframe的執行環境是獨立的!於是可以每次創建一個iframe,把它裏面的Object.prototype給拿來用!(聽上去挺浪費的是吧……)

代碼如下:
function nakedObject() {
var iframe = document.createElement('iframe')
iframe.width = iframe.height = 0
iframe.style.display = 'none'
document.appendChild(iframe)
iframe.src = 'javascript:'
var proto = iframe.contentWindow.Object.prototype
iframe.parentNode.removeChild(iframe)
iframe = null

var props = [
'constructor', 'hasOwnProperty', 'propertyIsEnumerable',
'isPrototypeOf', 'toLocaleString', 'toString', 'valueOf'
]
for (var i = 0; i < props.length; i++) {
delete proto[props[i]]
}
return proto
}


不過這個技巧確實相當浪費,僅僅爲了創建一個對象就建立了一整個iframe及其環境!那可相當於一個完整的網頁!在IE中創建幾千個這樣的對象,就需要好幾秒時間和幾十兆內存!所以這個技巧只能用於真正需要的地方,比如with。雖然同樣可以用於模擬Object.create(null)的行爲,但是是否真的有這樣的需求?或許更簡單的方式是把一個普通對象上從Object.prototype繼承來的屬性遮蔽掉(即設爲undefined),儘管這對於with來說不適合(原因請讀者自己想),但是對於其他大多數情況可能就夠用了。因此,我也不打算把這個trick提交給es5-shim項目,而只是僅用到我自己的一個項目中:[url=https://github.com/hax/my.js]my.js[/url]。

以上。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章