Object.create Reflect.setPrototypeOf 哪個比較好?

資料庫是海量json文件,讀到內存中作為緩存時需要把 JSON . parse 得來的對象變成沒有原型屬性的對象。方法有二:

obj=Object.assign(Object.create(null),obj)

Reflect.setPrototypeOf(obj,null)

哪種初始化運行效率高?

它們對後續使用的性能有差異嗎?

除了性能差異,內存開銷是否有所不同?

這裡的技術差異點主要是:一個是創建新的無繼承對象並賦值,一個是動態修改取消原型。


首先,你題干里沒說為什麼要把一個普通的對象(原型是 Object.prototype)轉成無原型對象(原型是 null),是為了性能?還是怕受原型鏈(其實只有 Object.prototype)上的屬性干擾?

如果是前者:

我目前是沒有聽說過無原型對象在屬性操作上比普通對象要快。但從規範上講:

1. 當你用 for-in 遍歷一個對象時,用普通對象的話,會多一次遍歷 Object.prototype 上所有可枚舉屬性的過程:

for (const key in {}); // 遍歷 Object.prototype

2. 當你讀取一個自身屬性里不存在的屬性時,用普通對象的話,會多一次在 Object.prototype 上的屬性查找過程:

const obj = {foo: 1}
obj.bar // 去查找 Object.prototype.bar, "bar" in obj 也會

3. 去賦值一個自身屬性里不存在的屬性時,用普通對象的話,也會多一次在 Object.prototype 上的屬性查找過程,如果找到了,還得判斷那個屬性是否只讀:

const obj = {foo: 1}
obj.bar = 2 // 去查找 Object.prototype.bar 並判斷其是否只讀

雖然理論上是這樣的,原型鏈越短越快。但實現和規範畢竟還有差異,在現實的引擎中,這些額外操作的費時說不定已經被優化到可以忽略不計(純無腦猜測),甚至說不定某些優化沒有應用到無原型對象身上(畢竟還是用 {} 的多),導致它的性能反而不好。

如果是後者:

無原型對象 99% 的使用場景都是把它當成其他語言里的 map/dict 來用的,而不是為了性能,比如:

const xxxMap = Object.create(null)

好處就是我上面說的,不會受 Object.prototype 上的屬性干擾,所以我猜你的出發點也是這個。

但這種好處的體現也有個前提,那就是「你在這個 map 對象身上將要讀取的屬性的名字不是硬編碼在程序里的,還是來自用戶輸入的」。

什麼意思呢,比如我上面建立的這個 xxxMap,我在代碼里寫死了要讀取的兩個屬性也就是 foo 和 bar,那我就沒必要擔心原型鏈的影響,因為我知道 Object.prototype 身上沒有 foo 和 bar(現如今已經沒必要考慮有第三方庫會在 Object.prototype 上寫東西,甚至 ES 規範從 ES3 之後也沒再沒添加過 Object.prototype 上的方法了)。但如果要讀取的屬性來自用戶輸入,那麼屬性可能是 toString、__proto__ 等等,就有麻煩了,有先例。

要考慮對象的樹狀結構

上面只是分析了一下你想做這件事情的可能的出發點,在正式回答你的問題之前,我還得說個事,那就是你題干里用的這兩行代碼,其實都是有問題的。因為單說這兩行代碼,它倆都只能將這個對象本身的原型改成 null,假如這個對象的某個屬性又是個對象,那麼那個對象的原型還是 Object.prototype:

const obj = {foo: {bar: 1}}
Object.setPrototypeOf(obj, null)
Object.getPrototypeOf(obj) // null
Object.getPrototypeOf(obj.foo) // Object.prototype
// Object.assign(Object.create(null), obj) 同理

既然你說了是海量 JSON 數據,那 parse 出來的對象肯定是包含很多級的。

所以正確的做法其實得在 JSON.parse 的回調函數里執行你的那兩行代碼,把 parse 出來的所有層級的對象都修改或替換成無原型對象:

JSON.parse(jsonString, (k, v) =&> {
if (({}).toString.call(v) === "[object Object]") {
return Object.assign(Object.create(null), v)
}
return v
})

JSON.parse(jsonString, (k, v) =&> {
if (({}).toString.call(v) === "[object Object]") {
return Object.setPrototypeOf(v, null)
}
return v
})

修改對象原型可能會影響對象之後的所有操作的效率

Firefox 在 3 年前實現 Object.setPrototypeOf() 的時候,認為修改對象原型的操作不僅這次操作本身就慢,更關鍵可能會讓這個對象之後的所有操作都變慢,於是他們在開發者工具加了個警告:

Object.setPrototypeOf() 的 MDN 頁面也加了一段明顯的警告文字,推薦人們盡量用 Object.create 在創建對象的時候就設置好想要的原型,而不是之後再改。

但現在開發者工具顯示的那個警告已經去掉了,原因是有 Google 的人反對。事情是這樣的,Traceur 是谷歌當時開發的 es6 轉譯器(已經停止開發),當時 Traceur 的 runtime 用到了 __proto__ 來修改對象原型,結果有用戶就在 Firefox 里看到了那個警告,給 Traceur 報了個 issue。

Traceur 的主要開發者、同時也是 V8 當時的主力開發 arv 看到了,認為不應該有這個警告,於是去給 Firefox 報了個 bug,讓他們去掉這個警告。arv 的理由是,修改原型有時候是必須的、是無法用 Object.create 來代替的,比如 Traceur 遇到的場景,是要把一個函數的原型從 Function.prototype 修改為別的,Object.create() 只能創建出 non exotic 的 object,也就是只能創建出 Object.prototype.toString(obj) === "[object Object]" 的對象,創建不出函數來,比如用 Object.create(Function.prototype) 創建的對象你肯定不能執行它,此外 Polymer 里也有類似的修改對象原型的代碼,arv 認為既然沒有替代的辦法,只能這麼做,那就不應該警告用戶讓用戶糾結。

這個警告最終在一年半之後(2016 年)被去掉了,同時 Firefox 的人寫了一篇詳細的文檔講解為什麼修改對象原型是不推薦的。

說兩句不相關的八卦,上面這件事情里我提到名字的 arv,其實是瀏覽器領域的老江湖了,從 2000 年開始就已經給 Mozilla 提 bug 和 patch 了,從 bugzilla 的 id 和註冊時間也能看出來。2005 年加入谷歌,2015 年離職,加入了一個創業公司,戲劇性的是,就在我剛剛點進它的 github 看時,發現他剛剛建立一個放簡歷的倉庫,看來是要找工作了。

到底哪個快還是得測試

按照常理推斷,從一個對象身上複製一堆屬性到另外一個對象身上肯定比簡單的修改一個對象的原型要慢吧,但到底是不是這樣只有測了才知道。巧的是,去年就已經有人測試過了,而且他的需求和你的需求一模一樣,也是要把 JSON.parse 出來的對象的原型變成 null,要對比的兩個實現方式也和你提供的也一樣,他們的結論就是在 Firefox 里修改原型的方式是要快一點,而在其他瀏覽器里兩者耗時相等,我自己剛剛測了一下,的確也是這樣的結論。

這才只回答了你題目里問的「哪種初始化運行效率高?」,至於第二問「它們對後續使用的性能有差異嗎?」,我沒有實測,但根據 MDN 的文檔來看,應該修改原型的方式是會影響引擎後續對這個對象的優化的。

畢竟別人也沒有你真實的海量 JSON 文件和真實的使用場景,還是自己動手測試一下吧。此刻在公司加班被一堆蚊子咬,草草結束提交回答回家了。


紫雲飛的答案已經很好,這裡只做少許補充。

問題關注性能差異,從這點說,避免修改原型才是確保性能的最佳方案。且不說引擎通常優先考慮常見場景而修改原型不在其列,就引擎性能優化的原理來說,修改原型必然是一個對性能不太友好的操作。何況還要對大量對象做這樣的操作。

理論上來說,這種需求很可能只有等到 realm api(tc39/proposal-realms)進入標準之後,才能有性能較好的解決方案。

不過我們現在可以曲線救國,創建一個 iframe,將該 iframe 中的 Object.prototype 上的屬性都 delete 掉,然後在該 iframe 中 JSON.parse 得到的對象雖然還是有原型的,但原型上並沒有其他屬性了。如果是在 node.js 中,可以使用 vm 模塊達到相同效果。這種方式基本沒有性能損耗。

不過雖然有上面這種奇技淫巧,但沒事兒最好別亂用。要求無原型這個需求確實是比較可疑的,如果是用於 dict 有其他簡單辦法(如加前綴)可規避原型屬性衝突問題,或者用 ES6 的 Map 即可,沒有必要採用無原型對象。

以上。


做個簡單的基準測試試試,再去parser的源碼里找找答案


初始化第二種應該要快不少

感覺不到差異吧

如果原始obj被回收了,內存應該也一樣

最後我想問一下,parse來的對象為啥要取消原型


推薦閱讀:

js中什麼技術能合併多個前端請求,並生成一個json文件發送?
Node.js 都應用在什麼項目上?這些項目為什麼選擇 Node.js?
如何利用mongodb+node.js完成一個搜索的功能?
有哪些比較好用的nodejs模塊?
有哪些值得推薦的針對 Node.js 本身而非 Express 框架之類的學習資料?

TAG:JavaScript | Nodejs | V8 |