岁月如歌

用开放的心态,打造专业的人生。

扩展原生对象与 es5-safe 模块

with 15 comments

扩展原生对象很邪恶吗

kangax 发表了一篇博文:Extending built-in native objects. Evil or not?, 很好地总结了扩展 JavaScript 内置原生对象的优劣:

首先要区分原生对象(Native Object, 比如 String/Array 等)和宿主对象(Host Object, 比如 DOM 等)。

扩展 Host Object 是邪恶的,很难实现,即便实现了也会有很多隐患,具体请参阅:What’s wrong with extending the DOM. 可以说对 DOM 对象进行扩展就注定了 Prototype.js 的没落(DOM extension is one of the biggest mistakes Prototype.js has ever done)。相比而言,jQuery 的作者 John Resig 没有选择 Protype.js 的老路,而是采用了 wrap 的方式,在 DOM 之上封装一套 API, 并且做到简洁易用,这是 jQuery 能兴起并流行至今的重要因素之一。

对 Native Object 进行扩展并不邪恶。kangax 的博文已详述,不多说。

扩展 Native Object 的难点在于,要实现与规范完全兼容的代码很难(writing proper, compliant shims is hard)。市面上流行的一些扩展方法,比如 Douglas Crockford 的 extend 方法,以及各个类库里的一些语言扩充,还有最近的一个项目 es5-shim 等,这些代码的实现都存在一些问题:Incorrent ES5 fallbacks.

如果要提供规范之外的一些方法,推荐采用 Underscore 的方式,以工具集的方式提供。这样能尽量避免冲突,并保持 API 的一致性。

es5-shim

很认同 hax 在 乱想 一文中关于 ES5 标准的一段话:

如果要产品化,特别是通用化,我认为考虑标准是及其重要的。这个标准,不仅是指现在已经有的纸面标准,而是要考虑标准的方向。

比方说过去大量的js库都是建立在一个小的方法集上的。但是新的库就应该注重base在ES5标准上。因为这是方向。不仅是纸面标准,而且也一定会是事实标准。在JS生存10年之后,我们必须认清楚,现在是一个跨越,就是开发者的baseline即将或已经提升到了ES5(比如nodejs上的开发社区),而不是之前残疾的ES3。

(此处省略 n 字,hax 知道…)

当然ES5这个baseline,是需要建立的,这就需要有库能把legacy浏览器弥补好。现在这个方向做的最好的是es5-shim。但是说实话,它真的还不够好。这块是有机会的,如果国内js高手们能联合起来,专注于这一个方向上做出世界级的库,绝不是天方夜谭。

es5-shim 开了一个很好的头,但就如 hax 所说,它真的不够好,目前的版本有以下不足:

  1. 有些方法,比如 Object.seal, 在老旧浏览器上很难甚至不可能实现。es5-shim 的策略是:fail silently. 就是说:让你调用,但不干活。这个策略在 es5-shim 的代码上随处可见,悲催呀。我期望的策略是:倘若无法实现某些特性,就爽快的抛出异常,让开发者自己去解决。
  2. 缺少测试。这两天作者补充了一些,但还非常欠缺。
  3. 代码实现上,太拘泥规范。比如 Function.prototype.bind 的实现,直接按照规范来一步步写代码,结果并不好,bugs 反而不少,还有些无效代码,比如给 bound.length 赋值。

总之,es5-shim 的理想是丰满的,但现实是骨感的。不少方法想完全按照规范来实现,但由于浏览器自身的限制,又无法完全实现,纠结中让使用者更纠结,隐患不少。

es5-safe

邮件给 es5-shim 的作者 kriskowal, 建议用 throw error 的策略来代替 fail silently. 但彼此很难说服,于是有了 es5-safe 项目:

https://github.com/seajs/dew/tree/master/src/es5-safe

es5-safe 模块里,仅扩展了可以较好实现的可以安全使用的部分方法,包括:

Function.prototype.bind
Object.create
Object.keys
Array.isArray
Array.prototype.forEach
Array.prototype.map
Array.prototype.filter
Array.prototype.every
Array.prototype.some
Array.prototype.reduce
Array.prototype.reduceRight
Array.prototype.indexOf
Array.prototype.lastIndexOf
String.prototype.trim
Date.now

大都是较好实现的“软柿子”,但也是最常用的方法。其中:

Function.prototype.bind
Object.create

是部分实现,比如 bind 返回的方法,会多出 prototype 属性,以及方法的 length 值不对。Object.create 方法不支持第二个参数,当你传入第二个参数时,会抛出错误。

采用 es5-safe 的好处是,可以放心大胆的使用,如果用错了,会收到 exception, 这样有助于将问题在开发或测试阶段快速解决掉。

此外,es5-safe 的代码质量很高。我这几天闲散时间全耗在这上面了,代码是我认为目前同类代码里质量最高的。单元测试直接来自 google V8 引擎,能有效保障代码功能的正确性。

目前在国内的主流浏览器(IE6-9, Chrome, Firefox, Safari, Opera)上均测试通过:

http://seajs.github.com/dew/src/es5-safe/test/runner.html

注意:并不完美

在三体世界里,完美的十维空间坍塌到现在的三维,整个世界就已经不完美了,连光速都是有限的-.- 我相信,绝大部分情况下,es5-safe.js 已够用,虽然离完美还有很远的距离。

这种不完美,主要体现在以下方面:

1. 规范本身的不完美。比如:

[].every(function(item) { return typeof item !== 'undefined'; } );

按照常理来看,应该返回 false. 但在当前所有浏览器下,均返回 true.

2. 浏览器对实现的限制。比如 Object.seal/frozen 的实现依赖浏览器自身的功能,在 Old IE 下,这是不可实现的任务。

3. 浏览器自身实现的差异性。比如 0 in [undefined]

  • 在原生 IE6-8 里,返回 false
  • 在原生 IE9 里,包括各种兼容模式下,返回 true
  • 非 IE 浏览器里,返回 true

这导致 Array.prototype.reduce 等方法,在操作对象为 [1,2,undefined,4] 这种含有 undefined 值的数组时,在原生 IE6-8 下的结果,与其他浏览器会有差异。

这种差异还有不少,大部分情况下不会遇到,但一旦遇到了,经常要找半天才能定位出原因。

小结

扩展 DOM 等 Host Object 是罪恶的,至少目前如此。

扩展原生 JavaScript 对象远没有想象中的糟糕。挑选一个合适的,比如 es5-safe.js, 让 es5 成为 baseline, 无论工作效率,还是心情,都是值当的。

面向未来开发!

About these ads

Written by lifesinger

August 10, 2011 at 15:59

Posted in Articles

15 Responses

Subscribe to comments with RSS.

  1. Array不应该有原型方法,也应该有静态方法啊。
    例如:可以这样用:Array.forEach(document.getElementsByTagName(‘a’),function(el){alert(el.innerHTML)})

    JK

    August 10, 2011 at 16:15

    • 漏了一个字:Array不应该只有原型方法,也应该有静态方法啊。

      JK

      August 10, 2011 at 16:17

      • 静态方法可以用 Array.protoype.forEach.call 方式,或在类库里自己封装一个就好,除了 Array.isArray, ES5 规范里没有定义其他静态方法。

        lifesinger

        August 10, 2011 at 22:28

  2. 0 in [undefined] 是一个array literal initialize的问题,就好象[,,,]应该返回一个长度为3的数组,而IE返回长度为4。这不是shim可解决的。实际上就是,在array literal initialization过程中,IE会跳过所有的undefined值(其他浏览器只会跳过[,,]中的值)。但是如果你是显式赋值,如a[3] = undefined或者a.push()等,其行为是正确的。幸好这个差异通常不会造成什么大的问题。

    hax

    August 11, 2011 at 15:59

  3. http://hax.iteye.com/blog/1146699 写了一篇blog谈我对fail silently的看法。
    另外kangax的观点大多赞同,但是对扩展DOM对象我并不完全同意他的观点,有时间再讨论。

    hax

    August 11, 2011 at 17:38

  4. 我看完了kangax的帖子后面的所有comments,发现我没有什么好补充的了,所有不同的意见在comments里已经非常充分了(特别是Sean Hogan的回复)。我唯一想补充的是:归根到底,对不可控对象的扩展总是有风险的,即使是wrapper方式也是如此。在其他语言里实际上也有类似的问题。JS的特殊之处是,原型扩展的影响是全局的,无法被局域化,这大大加剧了冲突的风险。抛开这部分共性的问题,造成困难的根本原因是在IE为代表的实现上的bug(及早期standards的缺乏)导致的实现问题。对于框架开发者来说,用wrapper方式只是规避了这些问题,降低了实现难度,但是对于使用者来说其实并没有啥额外好处(当然框架更少Bug也许可视为好处,但这个只是同义反复)。考虑到IE6/7迟早入土,一个未来的web框架扩展DOM仍然是选项之一。当然这必须是非常非常审慎的,总的来说。

    hax

    August 12, 2011 at 01:31

    • @hax 用 wrapper 的方式扩展 DOM,除了避免当前实现上的麻烦,我觉得还有一个更重要的因素是向用户提供便捷友好的 API. 比如 jQuery, 不仅仅是抹平浏览器之间的差异,还在此之上提供了更多便利的方法。DOM 原生的方法,即便没有浏览器兼容问题,用起来也不太便捷,属于中级 API. wrapper 提供的则往往是更友好的高级 API.

      lifesinger

      August 12, 2011 at 07:46

  5. […] 转自:扩展原生对象与 es5-safe 模块 « 岁月如歌. 08-132011 […]

  6. 去看了一眼. 居然有考虑 bound 被new 的情况?
    另外 似乎漏了一点. bound 不应该有 prototype属性.
    所以 应该补一句 bound.prototype = undefined;

    不过对于这个问题.似乎chrome15,之前的版本也没有遵守标准.不过15开始 bound的prototype也终于被搞成undefined.

    本来我有打算也写一套类似的东西. 现在看来没啥必要了.

    Franky

    August 26, 2011 at 17:41

    • es5-safe 目前的实现 bound 没有 prototype 属性的。除了 length 不对,以及 var b = new bound(); b instanceof bound 的问题(各浏览器的原生实现也不一致),其他都符合规范了。

      lifesinger

      August 27, 2011 at 08:37

  7. 好像里面并没有实现JSON对象, 是出于什么方面的考虑可以说说吗? 谢谢。

    zhengzongyi

    August 26, 2011 at 18:05

    • json 对象在 dew/src/json 模块里,Douglas 实现的版本非常不错。

      lifesinger

      August 27, 2011 at 08:38

  8. 哦 忘了说, bound.prototype 设为undefined,可能会有坑. instanceof 的时候会抛异常.
    但是相对的, v8引擎的对应实现也有bug. bound.[[Construct]] 实现虽然正确. 但是 bound.[[HasInstance]] 却实现错误.没有对 TargetFunction.[[HasInstance]] 进行wrap.

    总体来说. 完全模拟的话,看起来是软柿子. 真要较真.还真很麻烦.

    Franky

    August 26, 2011 at 18:51

    • 完全模拟挺难的,可以看这个 issue 中的进度:https://github.com/seajs/dew/issues/18

      lifesinger

      August 27, 2011 at 08:39

  9. […] 玉伯发布了es5-safe模块,这是一个有一点类似es5-shim的项目。 个人认为玉伯这个模块对于准备从ES3过渡到ES5的前端开发者来说是一个稳妥的选择。在本文的最后部分会进一步说明。下面的部分是理论性的探讨,无兴趣者可略过了。 es5-safe的缘起,是玉伯主张一个不太一样的策略,即“用 throw error 的策略来代替 fail silently”。 玉伯在《扩展原生对象与 es5-safe 模块》一文中写道:  […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 74 other followers