How not to Write Javascript

文: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行:

  1. for (var i = fromIndexi < arr.lengthi++) {

这个 for 循环每一次循环都查找了数组 (arr) 的.length 属性,简单的在开始循环的时候设置一个变量来存储这个数字,可以让循环跑得更快:

  1. for (var i = fromIndexii = arr.lengthi < iii++) {

Google的程序员们在同一个文件里面稍后的地方似乎发现了这个技巧,文件 array.js,153行:

  1. var l = arr.length;  // must be fixed during loop... see docs
  2. for (var i = l - 1i >= 0; --i) {

这个循环避免了在每次循环中的属性查找,但是这个for循环是如此的简单以至于它可以进一步的被简化成一个while循环,而且可以运行得更快:

  1. var i = arr.length;
  2. while (i--) {

但不是所有的Closure Library的效率都是由于没有优化好的循环造成的,文件 dom.js,797行:

  1. switch (node.tagName) {
  2.   case goog.dom.TagName.APPLET:
  3.   case goog.dom.TagName.AREA:
  4.   case goog.dom.TagName.BR:
  5.   case goog.dom.TagName.COL:
  6.   case goog.dom.TagName.FRAME:
  7.   case goog.dom.TagName.HR:
  8.   case goog.dom.TagName.IMG:
  9.   case goog.dom.TagName.INPUT:
  10.   case goog.dom.TagName.IFRAME:
  11.   case goog.dom.TagName.ISINDEX:
  12.   case goog.dom.TagName.LINK:
  13.   case goog.dom.TagName.NOFRAMES:
  14.   case goog.dom.TagName.NOSCRIPT:
  15.   case goog.dom.TagName.META:
  16.   case goog.dom.TagName.OBJECT:
  17.   case goog.dom.TagName.PARAM:
  18.   case goog.dom.TagName.SCRIPT:
  19.   case goog.dom.TagName.STYLE:
  20.   return false;
  21. }
  22. return true;

这类型的代码在Java中是相当普遍的,而且运行起来还不错。然而在JavaScript中,switch语句在每次一个程序员想要检查某个特定的HTML元素是否允许有子元素的时候都会低效的执行。

有经验的JavaScript程序员知道创建一个包含这个逻辑的object来做这个判断是快得多的:

  1. var takesChildren = {}
  2. takesChildren[goog.dom.TagName.APPLET] = 1;
  3. takesChildren[goog.dom.TagName.AREA] = 1;

建立这样一个object后,检查是否某个标签接收子元素的函数将运行的快得多:

  1. return !takesChildren[node.tagName];

这段代码可以进一步通过使用hasOwnProperty(下文有对此的详细解释)对外界干扰免疫:

  1. return !takesChildren.hasOwnProperty(node.tagName);

如果我们对Google有所期待的话,那就是执行效率了。好玩的是,Google发布了它自己的浏览器,Google Chrome,主要是为了提升JavaScript的执行效率到高一个层次!

看着这样的代码,我们不得不怀疑是不是Google通过培训他们自己的开发者写好一些的JavaScript代码也可以达到同样的目的。

漏水船中的六个月

说Google在构建Closure的时候忽略了开发效率是不公平的。实际上,这个library提供了一个通用的方法来缓存那些执行缓慢的函数的结果,这个方法被再次以同样的参数被调用的时候,结果会被立即返回。文件memoize.js,39行:

  1. goog.memoize = function(fopt_serializer) {
  2.   var functionHash = goog.getHashCode(f);
  3.   var serializer = opt_serializer || goog.memoize.simpleSerializer;
  4.   return function() {
  5.     // Maps the serialized list of args to the corresponding return value.
  6.     var cache = this[goog.memoize.CACHE_PROPERTY_];
  7.     if (!cache) {
  8.       cache = this[goog.memoize.CACHE_PROPERTY_] = {};
  9.     }
  10.     var key = serializer(functionHasharguments);
  11.     if (!(key in cache)) {
  12.       cache[key] = f.apply(thisarguments);
  13.     }
  14.     return cache[key];
  15.   };
  16. };

这是一个被很多大型JavaScript library采用的提升执行效率的聪明技巧;问题是,Google没有提供任何的方法来限制缓存的大小!当被缓存的方法只被很少的参数组合调用的时候这是没问题的,但这个方法如果通用的话就是危险的。

假如缓存一个方法的参数是鼠标的坐标位置的话,这段代码的内存占用将会失去控制的飞快增长,并且拖慢浏览器的速度。

用Dmitry的话来说就是:“我不太清楚在Java里面这个代码风格叫什么,但在JavaScript里面,这叫‘内存泄漏’”。

真空中的代码

是在他的关于开发一个JavaScript library的讲演中,Dmitry把JavaScript的全局作用域比做一个公共厕所。“你不能避免去那里”,他说,“但是如果可以的话尽量避免表面的接触。”

一个通用的JavaScript library如果要是可信赖的,它不仅仅要避免影响其他任何可能在同一空间运行的JavaScript代码,它同样要保护自身不被其它不那么礼貌的代码所影响。

在文件object.js,31行:

  1. goog.object.forEach = function(objfopt_obj) {
  2.   for (var key in obj) {
  3.     f.call(opt_objobj[key]keyobj);
  4.   }
  5. };

像这样的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来让代码更健壮:

  1. goog.object.forEach = function(objfopt_obj) {
  2.   for (var key in obj) {
  3.     if (obj.hasOwnProperty(key)) {
  4.       f.call(opt_objobj[key]keyobj);
  5.     }
  6.   }
  7. };

这是另一个Closure Library中特别脆弱的部分,来自 base.js, 667行:

  1. goog.isDef = function(val) {
  2.   return val !== undefined;
  3. };

这个函数检查一个特定的变量的值是否被定义。但如果有第三方的脚本将全局变量 undefined 设定为另一个值,它将会失效(stauren注:这是因为undefined在JavaScript中不是保留字)。只需要页面上任何一个位置有下面一行js就会把Closure Library搞崩溃:

  1. var undefined = 5;

依赖全局变量 undefined 是JavaScript library作者犯的另一个菜鸟错误。

你也许会想,那些乱给 undefined 变量赋值的人活该他们倒霉,但修正这个错误的代价是小的:简单的在函数内声明一个本地的 undefined 变量就好了!

  1. goog.isDef = function(val) {
  2.   var undefined;
  3.   return val !== undefined;
  4. };

混乱的类型

在其他语言的开发者看来,JavaScript中最让人迷惑的部分莫过于数据类型系统了。Closure Library包含这方面大量的错误,进一步显示了作者对于JavaScript这部分细节的经验缺乏。

文件 string.js, 97行:

  1. // We cast to String in case an argument is a Function. …
  2. var replacement = String(arguments[i]).replace();

这行代码使用了 String 转换函数把 arguments[i] 转换为一个字符串对象。这恐怕是做这样的一个转换的最慢的方式了,虽然对于其他语言的开发者来说这也许是最明显的办法。

一个快的多的方法是在你需要转换的值上面加一个空白字符串(“”):

  1. var replacement = (arguments[i] + "").replace();
  2. 下面是一个更和字符串相关的类型混乱。来自文件 base.js742行:
  3.  
  4. goog.isString = function(val) {
  5.   return typeof val == 'string';
  6. };

JavaScript实际上用两种方式来表现文本字符串——原生字符串类型和字符串对象:

  1. var a = "I am a string!";
  2. alert(typeof a)// Will output "string"
  3. var b = new String("I am also a string!");
  4. alert(typeof b)// Will output "object"

绝大多数时候用原生字符串类型来表示字符串是更有效的(上面的变量a),但要调用任何字符串上的原生的方法(例如toLowerCase),这个变量必须先被转换成一个字符串对象(上面的变量b)。JavaScript会在需要的时候自动的在2种类型之间转换。这个特性叫做“自动装箱(autoboxing)”,在很多其他的语言中也有。

不幸的是,在Google的只懂Java的程序员们眼中看来,Java只将字符串表示为对象。这是我对于为什么Closure Library会忽略JavaScript中第二种类型的字符串的最靠谱的猜想。

  1. var b = new String("I am also a string!");
  2. alert(goog.isString(b))// Will output FALSE
  3. 下面是另一个Java带来的类型混乱的例子。来自文件 <a href="http://code.google.com/p/closure-library/source/browse/trunk/closure/goog/color/color.js?r=2">color.js</a>,  633行:
  4.  
  5. return [
  6.   Math.round(factor * rgb1[0] + (1.0 - factor) * rgb2[0]),
  7.   Math.round(factor * rgb1[1] + (1.0 - factor) * rgb2[1]),
  8.   Math.round(factor * rgb1[2] + (1.0 - factor) * rgb2[2])
  9. ];

以上的那些 1.0 说明了问题。像Java这样的语言用 代表整形数据使用的(1)与代表浮点数据的(1.0)是不一样的。但在JavaScript中,数字类型就是数字类型。(1 – factor)一样会运行得很好。

另一个有着Java味道的JavaScript代码的例子可以在 fx.js 中找到,465行:

  1. goog.fx.Animation.prototype.updateCoords_ = function(t) {
  2.   this.coords = new Array(this.startPoint.length);
  3.   for (var i = 0i < this.startPoint.lengthi++) {
  4.     this.coords[i] = (this.endPoint[i] - this.startPoint[i]) * t +
  5.     this.startPoint[i];
  6.   }
  7. };

看到第二行里面他们是怎么构造一个数组的吗?

  1. this.coords = new Array(this.startPoint.length);

虽然在Java中这是必须的,但在JavaScript中在运行前指定数组的长度是完全没有意义的。这就和使用 var i = new Number(0); 而不是 var i=0; 来新建一个存储数字用的变量一样没有意义。

实际上,你可以只是简历一个空白的数组,让它自己在被填入值的时候自己变大。这样做代码不但更短,运行得也更快:

  1. 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。要完全的了解一个东西,最好各方的想法都看一看,如下:

你可能感兴趣的:(JavaScript,jquery,Google,prototype,chrome)