`
hax
  • 浏览: 951170 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

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

    博客分类:
  • JS
阅读更多
玉伯发布了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。比如:
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来说,其目的其实是防御性的。

如果代码在一个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版本,呵呵。




3
3
分享到:
评论
5 楼 hax 2011-08-15  
lifesinger 写道
明白了,我测试时忘了打开 strict 模式
就怕这种忘了导致的 bug, 特别是大团队合作时


确实,这里存在这种可能。除了“忘记”之外,还有一点就是目前浏览器的stable版本支持strict的并不多,如chrome/safari目前的stable版本尚不支持strict模式。NodeJS stable版本(0.4.x)也不支持strict模式。

所以目前要测试时,除了加上"use strict"外,还必须是在以下环境:

FF 4+
Chrome/Safari的beta版
NodeJS 0.5+
4 楼 lifesinger 2011-08-12  
明白了,我测试时忘了打开 strict 模式
就怕这种忘了导致的 bug, 特别是大团队合作时
我还是继续 es5-safe 先
等 ie6-7 可以不考虑了,再切换到 es5-shim, ie8 下貌似绝大部分都可以“安全”实现
3 楼 hax 2011-08-12  
另外,如果遵循了我所说的采用strict模式,并且不做“功能性的利用防御性方法”这样奇怪的事情,那么要兼容IE6所要记住的东西其实还挺少的。就是以下两点:

1. 不要用get/set
2. 不要依赖enumerable

其中第二点在未来可能会得到解决。


其他的坑基本上在strict模式下已经都填平了。不会漏到IE6那里去。


【当然坑总是有的,如lifesinger原文中提到的array literal initialization的坑,不过这种坑不是es5-shim或es5-safe可以解决的。】
2 楼 hax 2011-08-12  
@lifesinger

你的例子当中,create一个带有get accessor的对象。如我文中所述,es5-shim对此也是抛出异常的,尽管其文档说是fail silently。因此在这点上已经可以起到提示开发者的作用。

而我说seal这个防御性调用,通常fail silently是可接受的,这个前提是我们使用ES5 strict模式(也许我文中还不够强调此点)。

在strict模式下, obj.bar = 2 会扔出TypeError。因此不用到IE6里我们就已经发现错误了。

因此也不会有你所说的类库使用者调整代码的必要了。


唯一可能导致错误的情形是代码依赖于此种异常,如:

try {
   obj.bar = 2
} catch(e) {
   // do sth...
}

我很难想到一个合理的case,需要这样的代码。

即使真的有这样的需求,也很容易将捕捉异常改为测试方法:

if (Object.seal(obj) {
   // do sth...
}


总之,es5-shim的目标并非要“完美”支持,而是让在ES5环境里可以跑的程序也能不做改动跑在前es5环境中。

凡是真的无法运行在IE6中的,那在IE6中扔出异常是应该的。例如get/set。而seal这类防御性代码,在正常情形下,是不会影响代码逻辑的,因此fail silently是可接受的。当然我们可能犯错,所以我建议始终用strict模式,这样在FF调试时你已经能发现错误(比如对sealed对象的修改),这样这个bug就不会留到IE6中去。

如果开发者故意选择了non-strict模式,或者其代码依赖于对防御性代码的功能性运用(如利用try/catch),那他应该明确了解他在干什么,并承担后果,呵呵。

【也许我应将本文改写为《es5-shim最佳实践》?呵呵】




1 楼 lifesinger 2011-08-12  
同意 hax 的大部分分析

对于防御型的 api, 从目前的现状来讲,我觉得还是 throw error 比较好,因为这种 throw error, 是一种能将错误消灭在早期的优雅降级。比如:

在 Chrome 下,可以用:
var obj = Object.create({}, { 'foo': { get: function() { return 2; } } });
Object.seal(obj);

只要浏览器支持,一切没问题。

但如果你的应用要支持 IE6, 采用 throw error 策略,可以让开发者立刻意识到自己的代码不兼容 IE6, 从而做出调整,这种代价最晚在测试阶段就可发现,代价比较低。

如果用 fail sliently, 上面的在代码在 IE6 下不会有问题,问题很可能出现在代码的其他地方,比如:
obj.bar = 2;
var foo = obj.foo;
var bar = obj.bar;

在 Chrome 下 foo 的值是 2, bar 的值是 undefined,在 IE6 下,首先上面的代码得到的 foo 值和 bar 值和 Chrome 下是不一样的,导致的结果是运行时的逻辑错误,这种错误的定位成本经常很大。

为了解决上面的问题,首先功能型的 Object.create 对于无法完成的任务要抛出异常,对于防御型的 Object.seal, 个人觉得抛出异常,也可以让错误及早发现,这种异常不是封装 Object.seal, 而且在 IE6 下,就不提供 seal 方法,让开发者知道自己的代码在 IE6 下有隐患。

这种 throw error, 并非要 try catch 去解决,而是要求用 Object.seal 的开发者,要明确自己的运行环境,如果要支持 IE6,就不去用 Object.seal 特性,只用 es5-safe 的部分。

如果 fail silently 的话,麻烦留给的是类库使用者,类库使用者在发现问题后,需要调整代码:
obj.bar = 2;
var foo = obj.foo;
var bar = obj.bar;
if(foo !== 2) foo = 2;
if(bar === undefined) alert('fu*ck');

这样会让使用者发狂的。

要么完美支持,要么抛出异常,灰色地带很诱人但实在危险。基础类库的 fail silently,要做到无害,无隐患(ES5 的这些方法里好像没有)很难。

相关推荐

Global site tag (gtag.js) - Google Analytics