[JavaScript]Google Closure: 糟糕的JavaScript

原文:Google Closure: How not to write JavaScript

原译文:Google Closure: 糟糕的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ëlgRaphaë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 ;

依赖全局变量 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ëlgRaphaë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。要完全的了解一个东西,最好各方的想法都看一看,如下:

你可能感兴趣的:(JavaScript)