原文:Google Closure: How not to write JavaScript
译者注:google在2009年11月6号开源了自己在 gmail、google reader 等几乎所有重要 google 产品中使用的javascrpt : google closure ,包括一套庞大的类似与 dojo 的 library、一套与之相应的 compiler、一套 template 系统。closure 完成了很多事情,包括一直困扰前端开发们的开发效率和运行效率之间的平衡(closure 使用library来提升开发效率,使用侵入性极强的 compiler 来去除无用代码,保证执行效率缩减 js 的大小,这与 YUI 等 library 采用的 combo-handling 是不一样的思路,但对于单独的页面,js 的代码量将更少是肯定的)总之,google 这次开源 Closure 是一个很棒的事情,网上有这太多关于这件事的讨论,大家可以到文章结尾的相关链接处看到更多的相关讨论的文章。这里翻译的是一篇 sitepoint 上指出的一些 Closure 的 javascript 的细节处理的错误,虽然有这些 stupid 的部分,却并不妨碍 google closure 是一个伟大的工具(据创始人 erik 说,现在有超过400名google工程师贡献了closure的代码),在这种规模下,代码还是尽量的stupid一些好了。虽然这么说,了解一些聪明的javascript代码也并不会妨碍我们成为一个好的程序员,评价一个东西很糟糕也总是比创建一个新的东西容易得多。哈哈,废话不多说了,正文开始。
上周在澳大利亚佩恩的Edge of the Web会议上我碰到了javascript library Raphaël 和 gRaphaël 的创建者Dmitry Baranovskiy。这两个library做的最重要的事情也许就是使在javascript效率相对低下的IE上面绘制一些复杂的矢量图变得了可能。然而,Dmitry 却很不爽,因为他找到的一些实现的很糟糕的代码,在Google刚刚发布的Closure Library中。
在会议上做了一个名为how to write your own JavaScript library的演讲(详细笔记)之后,Dmitry在第二天早上早餐之后分享了他关于这个新library的想法:“就是这个世界现在需要的东西——另一个糟糕的JavaScript library”。当我问道是什么使它如此“糟糕”的时候,他解释说:“它是一个由不懂JavaScript的Java程序员们开发的JavaScript library。”
在那一天接下来的时间里面,Dmitry向那些愿意倾听的展示了他在Closure代码中发现的一个接一个的可怕的代码的例子。他告诉我,他最大的担忧是人们会因为Closure挂着强大的Google的招牌而从放弃一些真的很棒的例如jQuery这样的library转而使用它。
“我和你做个交易吧”,我告诉他,“给我一些可怕的代码的例子,我把他们发布在SitePoint上。”
缓慢的循环
文件 array.js,63行:
for (var i = fromIndex; i < arr.length; i++) {
这个 for 循环每一次循环都查找了数组 (arr) 的.length 属性,简单的在开始循环的时候设置一个变量来存储这个数字,可以让循环跑得更快:
for (var i = fromIndex, ii = arr.length; i < ii; i++) {
Google的程序员们在同一个文件里面稍后的地方似乎发现了这个技巧,文件 array.js,153行:
var l = arr.length; // must be fixed during loop... see docs ⋮ for (var i = l - 1; i >= 0; --i) {
这个循环避免了在每次循环中的属性查找,但是这个for循环是如此的简单以至于它可以进一步的被简化成一个while循环,而且可以运行得更快:
var i = arr.length; ⋮ while (i--) {
但不是所有的Closure Library的效率都是由于没有优化好的循环造成的,文件 dom.js,797行:
switch (node.tagName) { case goog.dom.TagName.APPLET: case goog.dom.TagName.AREA: case goog.dom.TagName.BR: case goog.dom.TagName.COL: case goog.dom.TagName.FRAME: case goog.dom.TagName.HR: case goog.dom.TagName.IMG: case goog.dom.TagName.INPUT: case goog.dom.TagName.IFRAME: case goog.dom.TagName.ISINDEX: case goog.dom.TagName.LINK: case goog.dom.TagName.NOFRAMES: case goog.dom.TagName.NOSCRIPT: case goog.dom.TagName.META: case goog.dom.TagName.OBJECT: case goog.dom.TagName.PARAM: case goog.dom.TagName.SCRIPT: case goog.dom.TagName.STYLE: return false; } return true;
这类型的代码在Java中是相当普遍的,而且运行起来还不错。然而在JavaScript中,switch语句在每次一个程序员想要检查某个特定的HTML元素是否允许有子元素的时候都会低效的执行。
有经验的JavaScript程序员知道创建一个包含这个逻辑的object来做这个判断是快得多的:
var takesChildren = {} takesChildren[goog.dom.TagName.APPLET] = 1; takesChildren[goog.dom.TagName.AREA] = 1;
建立这样一个object后,检查是否某个标签接收子元素的函数将运行的快得多:
return !takesChildren[node.tagName];
这段代码可以进一步通过使用hasOwnProperty(下文有对此的详细解释)对外界干扰免疫:
return !takesChildren.hasOwnProperty(node.tagName);
如果我们对Google有所期待的话,那就是执行效率了。好玩的是,Google发布了它自己的浏览器,Google Chrome,主要是为了提升JavaScript的执行效率到高一个层次!
看着这样的代码,我们不得不怀疑是不是Google通过培训他们自己的开发者写好一些的JavaScript代码也可以达到同样的目的。
漏水船中的六个月
说Google在构建Closure的时候忽略了开发效率是不公平的。实际上,这个library提供了一个通用的方法来缓存那些执行缓慢的函数的结果,这个方法被再次以同样的参数被调用的时候,结果会被立即返回。文件memoize.js,39行:
goog.memoize = function(f, opt_serializer) { var functionHash = goog.getHashCode(f); var serializer = opt_serializer || goog.memoize.simpleSerializer; return function() { // Maps the serialized list of args to the corresponding return value. var cache = this[goog.memoize.CACHE_PROPERTY_]; if (!cache) { cache = this[goog.memoize.CACHE_PROPERTY_] = {}; } var key = serializer(functionHash, arguments); if (!(key in cache)) { cache[key] = f.apply(this, arguments); } return cache[key]; }; };
这是一个被很多大型JavaScript library采用的提升执行效率的聪明技巧;问题是,Google没有提供任何的方法来限制缓存的大小!当被缓存的方法只被很少的参数组合调用的时候这是没问题的,但这个方法如果通用的话就是危险的。
假如缓存一个方法的参数是鼠标的坐标位置的话,这段代码的内存占用将会失去控制的飞快增长,并且拖慢浏览器的速度。
用Dmitry的话来说就是:“我不太清楚在Java里面这个代码风格叫什么,但在JavaScript里面,这叫‘内存泄漏’”。
真空中的代码
是在他的关于开发一个JavaScript library的讲演中,Dmitry把JavaScript的全局作用域比做一个公共厕所。“你不能避免去那里”,他说,“但是如果可以的话尽量避免表面的接触。”
一个通用的JavaScript library如果要是可信赖的,它不仅仅要避免影响其他任何可能在同一空间运行的JavaScript代码,它同样要保护自身不被其它不那么礼貌的代码所影响。
在文件object.js,31行:
goog.object.forEach = function(obj, f, opt_obj) { for (var key in obj) { f.call(opt_obj, obj[key], key, obj); } };
像这样的for-in循环在JavaScript library中是绝对危险的,因为你不会知道有其他的什么JavaScript代码可能在页面中运行,也不知道它可能会添加一些什么东西到JavaScript标准的Object.prototype中。(stauren注:这里是Dmitry不了解Closure的整个设计理念了,看过Closure Compiler的ADVANCE模式的高侵入式压缩方法就知道,它需求整个页面上有且仅有这一段js代码,否则编译会失败)
Object.prototype是一个包含着所有的JavaScript object共享属性的JavaScript object。给Object.prototype添加一个方法,当前页面上每一个JavaScript object都会包含这个方法——就算这个对象之前已经被创建!早期的像Prototype这样的JavaScript library 为Object.prototyp添加了大量各种的方便特性。
不幸的是,和Object.prototype中原生就有属性不一样,添加到Object.prototype的自定义属性会在任何页面上的for-in循环中被列举出来。
简单来说,Closure library不能与任何往Object.prototype添加特性的JavaScript代码共存。(stauren注:没错,google就是这么设计的。)
Google可以使用for-in循环中使用hasOwnProperty检查属性是否真的属于该object来让代码更健壮:
goog.object.forEach = function(obj, f, opt_obj) { for (var key in obj) { if (obj.hasOwnProperty(key)) { f.call(opt_obj, obj[key], key, obj); } } };
这是另一个Closure Library中特别脆弱的部分,来自 base.js, 667行:
goog.isDef = function(val) { return val !== undefined; };
这个函数检查一个特定的变量的值是否被定义。但如果有第三方的脚本将全局变量 undefined 设定为另一个值,它将会失效(stauren注:这是因为undefined在JavaScript中不是保留字)。只需要页面上任何一个位置有下面一行js就会把Closure Library搞崩溃:
var undefined = 5;p>依赖全局变量 undefined 是JavaScript library作者犯的另一个菜鸟错误。
你也许会想,那些乱给 undefined 变量赋值的人活该他们倒霉,但修正这个错误的代价是小的:简单的在函数内声明一个本地的 undefined 变量就好了!
goog.isDef = function(val) { var undefined; return val !== undefined; };
混乱的类型
在其他语言的开发者看来,JavaScript中最让人迷惑的部分莫过于数据类型系统了。Closure Library包含这方面大量的错误,进一步显示了作者对于JavaScript这部分细节的经验缺乏。
// We cast to String in case an argument is a Function. … var replacement = String(arguments[i]).replace(…);
这行代码使用了 String 转换函数把 arguments[i] 转换为一个字符串对象。这恐怕是做这样的一个转换的最慢的方式了,虽然对于其他语言的开发者来说这也许是最明显的办法。
一个快的多的方法是在你需要转换的值上面加一个空白字符串(”"):
var replacement = (arguments[i] + "").replace(…);
下面是一个更和字符串相关的类型混乱。来自文件 base.js,742行:
goog.isString = function(val) { return typeof val == 'string'; };
JavaScript实际上用两种方式来表现文本字符串——原生字符串类型和字符串对象:
var a = "I am a string!"; alert(typeof a); // Will output "string" var b = new String("I am also a string!"); alert(typeof b); // Will output "object"
绝大多数时候用原生字符串类型来表示字符串是更有效的(上面的变量a),但要调用任何字符串上的原生的方法(例如toLowerCase),这个变量必须先被转换成一个字符串对象(上面的变量b)。JavaScript会在需要的时候自动的在2种类型之间转换。这个特性叫做“自动装箱(autoboxing)”,在很多其他的语言中也有。
不幸的是,在Google的只懂Java的程序员们眼中看来,Java只将字符串表示为对象。这是我对于为什么Closure Library会忽略JavaScript中第二种类型的字符串的最靠谱的猜想。
var b = new String("I am also a string!"); alert(goog.isString(b)); // Will output FALSE
下面是另一个Java带来的类型混乱的例子。来自文件 color.js, 633行:
return [ Math.round(factor * rgb1[0] + (1.0 - factor) * rgb2[0]), Math.round(factor * rgb1[1] + (1.0 - factor) * rgb2[1]), Math.round(factor * rgb1[2] + (1.0 - factor) * rgb2[2]) ];
以上的那些 1.0 说明了问题。像Java这样的语言用 代表整形数据使用的(1)与代表浮点数据的(1.0)是不一样的。但在JavaScript中,数字类型就是数字类型。(1 – factor)一样会运行得很好。
另一个有着Java味道的JavaScript代码的例子可以在 fx.js 中找到,465行:
goog.fx.Animation.prototype.updateCoords_ = function(t) { this.coords = new Array(this.startPoint.length); for (var i = 0; i < this.startPoint.length; i++) { this.coords[i] = (this.endPoint[i] - this.startPoint[i]) * t + this.startPoint[i]; } };
看到第二行里面他们是怎么构造一个数组的吗?
this.coords = new Array(this.startPoint.length);
虽然在Java中这是必须的,但在JavaScript中在运行前指定数组的长度是完全没有意义的。这就和使用 var i = new Number(0); 而不是 var i=0; 来新建一个存储数字用的变量一样没有意义。
实际上,你可以只是遍历一个空白的数组,让它自己在被填入值的时候自己变大。这样做代码不但更短,运行得也更快:
this.coords = [];
啊,你们有没有注意到这个函数里面还有另外一个效率低下的for循环呢?
API 设计
如果所有这些底层的代码质量缺陷还不能让你信服,我觉得你应该试试Google在Closure Library中包含的一些API。
例如Closure里面的图形类(graphics classes),是以HTML5 canvas API为基础构建的,你应该很奇怪为什么一个JavaScript API会以一个HTML标准来设计。简单来说,这是冗余、低效的,完全比不上同类代码。
作为Raphaël 和 gRaphaël 的作者,Dmitry在设计可用的JavaScript API方面相当有经验。如果你想感受一下canvas API的全部恐怖(当然,Closure的图形API也有所贡献),看看Dmitry在Web Directions South 2009讲演上面关于这个话题的音频和ppt吧。
Goolgle对于代码质量的责任
到这个时候我想你应该确信了在网上的最好的JavaScript代码中,Closure Library不是一个闪闪发光的明星了。如果你想找的是这样的代码,我可以向你你推荐一下更声名远扬的就像jQuery这样的library吗?
但你也许会想“这又怎么样?Google想发布什么垃圾代码就发布什么垃圾代码——又没人强迫你用它。”如果这是一个某google员工以自己名义发布的个人项目,我同意这个观点,但Google通过给Closure Library打上Google 商标的行为认可了它。
事实上,程序员们会因为 Closure Library 有着Google的名字而使用它,这就真的是一个杯具了。你喜欢也罢不喜欢也罢,Google在开发社区中是一个被信任的名字,所以Google应该抱着对开发社区负责的态度,在决定像Closure这样的library是否值得向公众曝光之前好好的自己检查一下。
—
译者注:说it sucks总是很容易,Closure自然有种种的不足,不过完全没有抹杀它为JavaScript界带来的一些新想法,包括强大的Google Compiler。要完全的了解一个东西,最好各方的想法都看一看,如下: