shim是应该抛异常还是应该fail silently?

http://hax.iteye.com/blog/1146699 【2011-08-11 17:26

玉伯发布了es5-safe模块,这是一个有一点类似es5-shim的项目。 

个人认为玉伯这个模块对于准备从ES3过渡到ES5的前端开发者来说是一个稳妥的选择。在本文的最后部分会进一步说明。下面的部分是理论性的探讨,无兴趣者可略过了。 



es5-safe的缘起,是玉伯主张一个不太一样的策略,即“用 throw error 的策略来代替 fail silently”。 

玉伯在《扩展原生对象与 es5-safe 模块》一文中写道: 

玉伯 写道
有些方法,比如 Object.seal, 在老旧浏览器上很难甚至不可能实现。es5-shim 的策略是:fail silently. 就是说:让你调用,但不干活。这个策略在 es5-shim 的代码上随处可见,悲催呀。我期望的策略是:倘若无法实现某些特性,就爽快的抛出异常,让开发者自己去解决。



这里我想探讨一下这个问题,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。比如: 

Javascript代码   收藏代码
  1. Object.seal(o)  
  2. if (!Object.isSealed(o)) {  
  3.    // fallback  
  4. }  



相比较扔异常,我认为这才是合适的写法。ES5程序员并不会期待Object.seal()扔出异常。如果强制他们为shim去捕捉异常是不合适的,违背了shim的初衷。所以需要进行fallback的人应该通过其他手段去测试代码是否有效。 

当然这里对isSealed的调用也是有些奇特的,通常这是一个不会被运行到的死分支。也许更明确的写法是: 

Javascript代码   收藏代码
  1. Object.seal(o)  
  2. try {  
  3.    assert (Object.isSealed(o))  
  4. catch(e) {  
  5.    // fallback  
  6. }  


不过我觉得这样写有点太腐儒了(try一个assert似乎也很诡异,通常我们只会在测试代码中这样写),前一个写法加一点注释就已经足够了。 

我们再进一步分析一下seal的用途。 

对于seal来说,其目的其实是防御性的。 

如果代码在一个ES5引擎的strict模式下能正确执行(strict模式会对不安全行为如对sealed对象改变属性扔异常),则在shim环境下通常不会出错(除非你的代码依赖于在strict模式中故意触发异常!没有正常人会这样写程序)。这也是我在广州演讲上的要点,鼓励大家用strict模式,而shim应该是配合strict模式用的。 

既然代码的安全性(即扔异常这种行为)已经由strict模式保证了,那么shim的fail silently也是可以接受的了。归根到底,shim可以被视同为非strict模式,而非strict模式其实就是大量采用了fail silently的方式。 

综上所述,对于Object.seal()来说,shim选择fail silently是可取的。 


或许问题主要在Object.defineProperty/Object.create上。这些方法不是单纯防御性的,而是功能性的。调用这些方法会改变一些事关重大的行为,比如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来进行属性遍历。【这可以通过如JSHint这样的工具加以保证。】 


总结一下。我认为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版本,呵呵。 

 

你可能感兴趣的:(异常)