扩展原生对象与 es5-safe 模块
扩展原生对象很邪恶吗
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 所说,它真的不够好,目前的版本有以下不足:
- 有些方法,比如 Object.seal, 在老旧浏览器上很难甚至不可能实现。es5-shim 的策略是:fail silently. 就是说:让你调用,但不干活。这个策略在 es5-shim 的代码上随处可见,悲催呀。我期望的策略是:倘若无法实现某些特性,就爽快的抛出异常,让开发者自己去解决。
- 缺少测试。这两天作者补充了一些,但还非常欠缺。
- 代码实现上,太拘泥规范。比如 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, 无论工作效率,还是心情,都是值当的。
面向未来开发!