shim是應該拋異常還是應該fail silently?

玉伯發佈了[url=https://github.com/seajs/dew/tree/master/src/es5-safe]es5-safe模塊[/url],這是一個有一點類似[url=https://github.com/hax/es5-shim]es5-shim[/url]的項目。

個人認爲玉伯這個模塊對於準備從ES3過渡到ES5的前端開發者來說是一個穩妥的選擇。在本文的最後部分會進一步說明。下面的部分是理論性的探討,無興趣者可略過了。


es5-safe的緣起,是玉伯主張一個不太一樣的策略,即“用 throw error 的策略來代替 fail silently”。

玉伯在《[url=http://lifesinger.wordpress.com/2011/08/10/extending-built-in-native-objects/]擴展原生對象與 es5-safe 模塊[/url]》一文中寫道:
[quote="玉伯"]有些方法,比如 Object.seal, 在老舊瀏覽器上很難甚至不可能實現。es5-shim 的策略是:fail silently. 就是說:讓你調用,但不幹活。這個策略在 es5-shim 的代碼上隨處可見,悲催呀。我期望的策略是:倘若無法實現某些特性,就爽快的拋出異常,讓開發者自己去解決。[/quote]

這裏我想探討一下這個問題,shim是應該throw error還是fail silently?

首先,我的觀點,對於Object.seal()來說,fail silently是合適的策略。

我不知道ecma262委員會是否針對每個API討論過這個問題——不支持某個行爲時拋出異常——然而如果讓我選擇,至少對於seal,一定會選擇現在的方式,因爲要求程序員去try catch然後fallback基本上是無意義的。

可以問這樣一個問題:對於seal的調用存在某種fallback嗎?

答案通常是否定的。

假設存在某種具有普遍適用性的fallback(比如對於IE DOM object,設置expando = false),那麼shim實現中就可以直接加入,不必勞煩每個程序員自己去做。

個人感覺,這個問題其實和java的聲明throws有點類似。理論上說,爲了嚴格的類型安全,應該每層都聲明throws,但是實際結果是較爲糟糕的。

因爲在絕大多數case裏,程序員除了捕捉exception,包裝一下,繼續向上一層throw,就沒別的選擇。【更糟糕的是,這鼓勵了兩種糟糕的慣例:A. 使用IDE生成的try/catch骨架代碼,但是catch之後啥也不幹——直接退化成silently fail!B. 總向上拋,以至於應該有fallback時也習慣性的向上throw,結果總是退化成Fatal Error。】

反過來說,如果有fallback,那麼即使不強制throws,一樣可以在合適的層次catch。

回到ES5 shim的例子,即使是fail silently,如果你確實有某種fallback,則一樣可以加上去,我們不用try來捕捉,而是可以通過簡單的測試代碼來確定它是否是真的sealed。比如:

Object.seal(o)
if (!Object.isSealed(o)) {
// fallback
}


相比較扔異常,我認爲這纔是合適的寫法。ES5程序員並不會期待Object.seal()扔出異常。如果強制他們爲shim去捕捉異常是不合適的,違背了shim的初衷。所以需要進行fallback的人應該通過其他手段去測試代碼是否有效。

當然這裏對isSealed的調用也是有些奇特的,通常這是一個不會被運行到的死分支。也許更明確的寫法是:

Object.seal(o)
try {
assert (Object.isSealed(o))
} catch(e) {
// fallback
}

不過我覺得這樣寫有點太腐儒了(try一個assert似乎也很詭異,通常我們只會在測試代碼中這樣寫),前一個寫法加一點註釋就已經足夠了。

我們再進一步分析一下seal的用途。

對於seal來說,其目的其實是[b]防禦性[/b]的。

如果代碼在一個ES5引擎的strict模式下能正確執行(strict模式會對不安全行爲如對sealed對象改變屬性扔異常),則在shim環境下通常不會出錯(除非你的代碼依賴於在strict模式中故意觸發異常!沒有正常人會這樣寫程序)。這也是我在[url=http://hax.iteye.com/blog/1137735]廣州演講[/url]上的要點,鼓勵大家用strict模式,而[b]shim應該是配合strict模式[/b]用的。

既然代碼的安全性(即扔異常這種行爲)已經由strict模式保證了,那麼shim的fail silently也是可以接受的了。歸根到底,shim可以被視同爲非strict模式,而非strict模式其實就是大量採用了fail silently的方式。

綜上所述,對於Object.seal()來說,shim選擇fail silently是可取的。


或許問題主要在Object.defineProperty/Object.create上。這些方法不是單純防禦性的,而是[b]功能性[/b]的。調用這些方法會改變一些事關重大的行爲,比如get/set,比如enumerable(影響in和for...in)。【而writable和configurable都是防禦性的。】

目前,對於get/set定義,es5-shim是扔異常的(我的fork版本則會區分DOM對象和native對象,只在真的無法定義時才扔異常)。由此可見,es5-shim也並非全部都fail silently。【雖然其文檔上對get/set寫的是fail silently——這是個文檔錯誤。】

剩下的問題是enumerable。這個問題確實比較大。這也是我對es5-shim不太滿意的地方。目前我的fork版本已經修復了Object.keys和Object.getOwnPropertyNames的一些bug,但enumerable的基本問題是es5-shim壓根忽略它。而我認爲這塊其實是可以實現出來的。一旦我們有較爲可靠的enumerable,則我們就可以放棄使用for...in(並逐次調用hasOwnProperty),而是用Object.keys來進行屬性遍歷。【這可以通過如[url=http://jshint.com/]JSHint[/url]這樣的工具加以保證。】


總結一下。我認爲es5-shim的基本原則是可取的。是否fail silently應根據各種因素綜合考慮。對於es5-shim來說,凡防禦性的方法採用fail silently策略是可取的。而其他部分則需要謹慎考量。【這建立在一個前提條件下:即開發者採用es5 strict模式,而用es5-shim作爲兼容方案。】


值得注意的是,其實es5-shim的文檔已經把API分爲了Safe Shims、存疑的Shims(以/?\標記)和目前尚不完善的Shims(以/!\標記),雖然其分類未必全然準確(如Object.keys對於es5-shim來說應該屬於存疑的Shims而不是Safe Shims)。

從這個意義上說,單獨抽取一個es5-safe模塊意義並非最大。


但是es5-safe仍然是一個很不錯的選擇,主要的好處我認爲是以下幾點:

1. es5-safe比es5-shim要小巧很多。
2. es5-safe的部分實現可能比當前es5-shim要更好。
3. es5-safe採用了一個保守策略。

特別是第三點,保守策略在很多時候是更好的選擇——儘管我本人一貫主張並實踐更激進的策略。

因爲保守策略意味着穩妥,可以避免踩地雷。

以es5-shim爲例,我在配合使用es5-shim和traits.js的時候,發生了許多問題。這是因爲es5-shim存在的一些bug,這些bug只有在像traits.js這樣大量依賴defineProperty的庫中才會暴露出來。爲了修復這些bug,我花了大約2個工作日。對於許多工期緊張的項目來說,在基礎庫上花這樣的時間和精力恐怕是不可接受的。

當然,即使使用es5-shim,你仍然可以只用那些標記爲Safe的shims,不過這種靠自覺的約束通常不太現實。在沒有碰到問題之前,你怎麼知道會碰到問題呢?


因此,對於大多數國內的前端開發人員來說,我覺得es5-safe在一段時期內可能是一個比es5-shim更穩妥的選擇。什麼時候es5-shim更加完善了,或者其他類似的較完善的項目,我們再切換過去。這個過渡期內,我們使用的其實是一個ES5的降級版本,它適應於目前仍然將IE6列入基本支持目標的現實。


當然,未來歸根到底是屬於ES5的(或許還有ES6)。條件許可的情況下,比如在個人項目、預研性項目,或者時間較爲寬鬆的情況下,我還是鼓勵大家嘗試es5-shim,尤其是我的fork版本,呵呵。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章