• (转)Google Closure: 糟糕的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ë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;
    
    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ë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。要完全的了解一个东西,最好各方的想法都看一看,如下:

  • 相关阅读:
    shell 脚本实现yum安装 LAMP 架构的 wordpress
    redis主从及哨兵和cluster集群
    扫描网段中服务器显示状态
    利用for循环输出九九乘法表
    正则表达式取文件后缀
    利用正则表达式实现yes/no判断
    判断输入的IP地址是否合法
    Shell脚本编程基础之shell脚本条件测试命令
    Shell脚本编程基础之shell脚本逻辑运算
    Shell脚本编程基础之shell脚本算术运算
  • 原文地址:https://www.cnblogs.com/rubylouvre/p/1615593.html
Copyright © 2020-2023  润新知