了解 Closure 编译器施加的限制

Closure 编译器希望其 JavaScript 输入符合几项限制。您要求编译器执行的优化级别越高,编译器对输入 JavaScript 的限制就越多。

本文档介绍了每种优化级别的主要限制。 如需了解编译器做出的其他假设,另请参阅此 Wiki 页面

针对所有优化级别的限制

编译器会针对每个优化级别对其处理的所有 JavaScript 施加以下两项限制:

  • 编译器只能识别 ECMAScript。

    ECMAScript 5 是几乎所有地方支持的 JavaScript 版本。不过,编译器也支持 ECMAScript 6 中的许多功能。 编译器仅支持官方语言功能。

    符合相应 ECMAScript 语言规范的浏览器特定功能可以与编译器搭配使用。例如,ActiveX 对象是使用合法的 JavaScript 语法创建的,因此用于创建 ActiveX 对象的代码适用于编译器。

    编译器维护人员会积极努力为新语言版本及其功能提供支持。项目可以使用 --language_in 标志指定其要使用的 ECMAScript 语言版本。

  • 编译器不会保留注释。

    所有编译器优化级别都会移除注释,因此依赖于特殊格式的注释的代码不适用于编译器。

    例如,由于编译器不会保留注释,因此您无法直接使用 JScript 的“条件注释”。不过,您可以通过将条件注释封装在 eval() 表达式中来解决此限制。编译器可以处理以下代码,而不会生成错误:

     x = eval("/*@cc_on 2+@*/ 0");
    

    注意:您可以使用 @preserve 注解在编译器输出的顶部添加开源许可和其他重要文本。

对 SIMPLE_OPTIMIZATIONS 的限制

简单的优化级别会重命名函数参数、局部变量和本地定义的函数,以缩减代码大小。不过,某些 JavaScript 结构可能会破坏此重命名过程。

使用 SIMPLE_OPTIMIZATIONS 时,请避免使用以下结构和做法:

  • with

    使用 with 时,编译器无法区分局部变量和同名的对象属性,因此它会重命名该名称的所有实例。

    此外,with 语句会使代码更难被人阅读。with 语句更改名称解析的常规规则,甚至可能会让编写代码的程序员很难识别名称引用的内容。

  • eval()

    编译器不会解析 eval() 的字符串参数,因此不会重命名该参数中的任何符号。

  • 函数或参数名称的字符串表示形式

    编译器会重命名函数和函数参数,但不会更改代码中按名称引用函数或参数的任何字符串。因此,您应避免在代码中将函数或参数名称表示为字符串。例如,原型库函数 argumentNames() 会使用 Function.toString() 来检索函数参数的名称。但 argumentNames() 可能会诱使您在代码中使用参数名称,而简单模式编译则会破坏此类引用。

针对 ADVANCED_OPTIMIZATION 的限制

ADVANCED_OPTIMIZATIONS 编译级别可执行与 SIMPLE_OPTIMIZATIONS 相同的转换,还添加了对属性、变量和函数进行全局重命名、无代码消除和属性展平的功能。这些新卡券会对输入 JavaScript 施加额外的限制。一般来说,使用 JavaScript 的动态功能会导致代码无法进行正确的静态分析。

全局变量、函数和属性重命名的影响:

ADVANCED_OPTIMIZATIONS 的全局重命名会使以下做法变得危险:

  • 未声明的外部引用

    为了正确重命名全局变量、函数和属性,编译器必须知道对这些全局变量的所有引用。您必须告知编译器有关正在编译的代码之外定义的符号。高级编译和 Extern 说明了如何声明外部符号。

  • 在外部代码中使用未导出的内部名称

    经过编译的代码必须导出未编译代码引用的任何符号。高级编译和 Extern 说明了如何导出符号。

  • 使用字符串名称引用对象属性

    编译器会在高级模式下重命名属性,但绝不会重命名字符串。

      var x = { renamed_property: 1 };
      var y = x.renamed_property; // This is OK.
    
      // 'renamed_property' below doesn't exist on x after renaming, so the
      //  following evaluates to false.
      if ( 'renamed_property' in x ) {}; // BAD
    
      // The following also fails:
      x['renamed_property']; // BAD
    

    如果您需要使用带引号的字符串引用属性,请始终使用带英文引号的字符串:

      var x = { 'unrenamed_property': 1 };
      x['unrenamed_property'];  // This is OK.
      if ( 'unrenamed_property' in x ) {};   // This is OK
    
  • 将变量引用为全局对象的属性

    编译器单独重命名属性和变量。例如,虽然编译器对 foo 的两个引用是等效的,但两者的处理方式是不同的:

      var foo = {};
      window.foo; // BAD
    

    此代码可能会编译为:

      var a = {};
      window.b;
    

    如果您需要将变量作为全局对象的属性进行引用,请始终以这种方式引用它:

    window.foo = {}
    window.foo;
    

Implications of dead code elimination

The ADVANCED_OPTIMIZATIONS compilation level removes code that is never executed. This elimination of dead code makes the following practices dangerous:

  • Calling functions from outside of compiled code:

    When you compile functions without compiling the code that calls those functions, the Compiler assumes that the functions are never called and removes them. To avoid unwanted code removal, either:

    • compile all the JavaScript for your application together, or
    • export compiled functions.

    Advanced Compilation and Externs describes both of these approaches in greater detail.

  • Retrieving functions through iteration over constructor or prototype properties:

    To determine whether a function is dead code, the Compiler has to find all the calls to that function. By iterating over the properties of a constructor or its prototype you can find and call methods, but the Compiler can't identify the specific functions called in this manner.

    For example, the following code causes unintended code removal:

    function Coordinate() {
    }
    Coordinate.prototype.initX = function() {
      this.x = 0;
    }
    Coordinate.prototype.initY = function() {
      this.y = 0;
    }
    var coord = new Coordinate();
    for (method in Coordinate.prototype) {
      Coordinate.prototype[method].call(coord); // BAD
    }
        

    编译器未理解 initX()initY()for 循环中被调用,因此它会移除这两种方法。

    请注意,如果您将函数作为参数传递,编译器就会找到对该参数的调用。例如,在高级模式下编译以下代码时,编译器不会移除 getHello() 函数。

    function alertF(f) {
      alert(f());
    }
    function getHello() {
      return 'hello';
    }
    // The Compiler figures out that this call to alertF also calls getHello().
    alertF(getHello); // This is OK.
        

对象属性展平的影响

在高级模式下,编译器会收起对象属性,以便为名称缩短做好准备。例如,编译器会转换以下代码:

   var foo = {};
   foo.bar = function (a) { alert(a) };
   foo.bar("hello");

转换为:

   var foo$bar = function (a) { alert(a) };
   foo$bar("hello");

通过该属性展平,可以提高之后对重命名过程的重命名效率。例如,编译器可以将 foo$bar 替换为单个字符。

但是,属性扁平化还会导致以下危险:

  • 在构造函数和原型方法之外使用 this

    属性展平可以更改函数中关键字 this 的含义。例如:

       var foo = {};
       foo.bar = function (a) { this.bad = a; }; // BAD
       foo.bar("hello");
    

    变为:

       var foo$bar = function (a) { this.bad = a; };
       foo$bar("hello");
    

    在转换之前,foo.bar 中的 this 引用 foo。转换后,this 是指全局 this。在此类情况下,编译器会生成以下警告:

    "WARNING - dangerous use of this in static method foo.bar"

    为防止属性展平中断对 this 的引用,请仅在构造函数和原型方法中使用 this。当您使用 new 关键字调用构造函数时,或在 prototype 的属性函数中调用构造函数时,this 的含义不明确。

  • 在不知道调用哪个类的情况下使用静态方法

    例如,如果您有:

    class A { static create() { return new A(); }};
    class B { static create() { return new B(); }};
    let cls = someCondition ? A : B;
    cls.create();
    
    编译器将收起这两种 create 方法(在从 ES6 转换为 ES5 之后),因此 cls.create() 调用将失败。您可以通过 @nocollapse 注解避免此问题:
    class A {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    class B {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    

  • 在不知道父类的情况下在静态方法中使用 super:

    以下代码是安全的,因为编译器知道 super.sayHi() 引用了 Parent.sayHi()

    class Parent {
      static sayHi() {
        alert('Parent says hi');
      }
    }
    class Child extends Parent {
      static sayHi() {
        super.sayHi();
      }
    }
    Child.sayHi();
    

    不过,即使 myMixin(Parent).sayHi 等于未编译的 Parent.sayHi,属性扁平化也会破坏以下代码:

    class Parent {
      static sayHi() {
        alert('Parent says hi');
      }
    }
    class Child extends myMixin(Parent) {
      static sayHi() {
        super.sayHi();
      }
    }
    Child.sayHi();
    

    使用 /** @nocollapse */ 注解可避免这种破坏。

  • 使用 Object.defineProperties 或 ES6 getter/setter

    编译器不能很好地理解这些结构。ES6 getter 和 setter 通过转译转换为 Object.defineProperties(...)。目前,编译器无法静态分析此构造,并假定属性访问和设置没有附带效应。这会带来危险的影响。例如:

    class C {
      static get someProperty() {
        console.log("hello getters!");
      }
    }
    var unused = C.someProperty;
    

    编译为:

    C = function() {};
    Object.defineProperties(C, {a: // Note someProperty is also renamed to 'a'.
      {configurable:!0, enumerable:!0, get:function() {
        console.log("hello world");
        return 1;
    }}});
    

    经确定,C.someProperty 没有任何副作用,因此已被移除。