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版本,呵呵。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章