Понимание ограничений, налагаемых компилятором закрытия

Компилятор Closure ожидает, что входные данные JavaScript будут соответствовать нескольким ограничениям. Чем выше уровень оптимизации, который вы просите выполнить компилятор, тем больше ограничений компилятор накладывает на входной JavaScript.

В этом документе описаны основные ограничения для каждого уровня оптимизации. См. также эту вики-страницу для дополнительных предположений, сделанных компилятором.

Ограничения для всех уровней оптимизации

Компилятор накладывает следующие два ограничения на весь обрабатываемый им JavaScript для всех уровней оптимизации:

  • Компилятор распознает только ECMAScript.

    ECMAScript 5 — это версия JavaScript, поддерживаемая почти повсеместно. Однако компилятор также поддерживает многие функции ECMAScript 6. Компилятор поддерживает только функции официального языка.

    Специфичные для браузера функции, которые соответствуют соответствующей спецификации языка ECMAScript, будут нормально работать с компилятором. Например, объекты ActiveX создаются с использованием допустимого синтаксиса JavaScript, поэтому код, создающий объекты ActiveX, работает с компилятором.

    Специалисты по сопровождению компилятора активно работают над поддержкой новых языковых версий и их функций. Проекты могут указать, какую языковую версию ECMAScript они предполагают, используя флаг --language_in .

  • Компилятор не сохраняет комментарии.

    Все уровни оптимизации компилятора удаляют комментарии, поэтому код, основанный на специально отформатированных комментариях, не работает с компилятором.

    Например, поскольку компилятор не сохраняет комментарии, вы не можете напрямую использовать «условные комментарии» JScript. Однако вы можете обойти это ограничение, заключив условные комментарии в выражения eval() . Компилятор может обработать следующий код без возникновения ошибки:

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

    Примечание. Вы можете включить лицензии с открытым исходным кодом и другой важный текст в верхней части выходных данных компилятора, используя аннотацию @preserve.

Ограничения для SIMPLE_OPTIMIZATIONS

Уровень оптимизации Simple переименовывает параметры функций, локальные переменные и локально определенные функции для уменьшения размера кода. Однако некоторые конструкции JavaScript могут нарушить этот процесс переименования.

Избегайте следующих конструкций и методов при использовании SIMPLE_OPTIMIZATIONS :

  • with :

    При with компилятор не может отличить локальную переменную от свойства объекта с тем же именем, и поэтому переименовывает все экземпляры этого имени.

    Кроме того, оператор with делает ваш код более трудным для чтения людьми. Оператор with изменяет обычные правила разрешения имен и может затруднить определение того, на что ссылается имя, даже программисту, написавшему код.

  • eval() :

    Компилятор не анализирует строковый аргумент eval() , поэтому он не будет переименовывать какие-либо символы в этом аргументе.

  • Строковые представления имен функций или параметров:

    Компилятор переименовывает функции и параметры функций, но не изменяет никаких строк в вашем коде, которые ссылаются на функции или параметры по имени. Таким образом, вам следует избегать представления имен функций или параметров в виде строк в вашем коде. Например, функция argumentNames() библиотеки Prototype использует Function.toString() для получения имен параметров функции. Но в то время как argumentNames() может соблазнить вас использовать имена аргументов в вашем коде, компиляция в простом режиме нарушает этот вид ссылок.

Ограничения для ADVANCED_OPTIMIZATIONS

Уровень компиляции ADVANCED_OPTIMIZATIONS выполняет те же преобразования, что и SIMPLE_OPTIMIZATIONS , а также добавляет глобальное переименование свойств, переменных и функций, удаление мертвого кода и выравнивание свойств. Эти новые проходы накладывают дополнительные ограничения на входной JavaScript. Как правило, использование динамических функций JavaScript препятствует корректному статическому анализу вашего кода.

Последствия переименования глобальных переменных, функций и свойств:

Глобальное переименование ADVANCED_OPTIMIZATIONS делает следующие действия опасными:

  • Необъявленные внешние ссылки:

    Чтобы правильно переименовать глобальные переменные, функции и свойства, компилятор должен знать обо всех ссылках на эти глобальные переменные. Вы должны сообщить компилятору о символах, которые определены вне компилируемого кода. Advanced Compilation and Externs описывает, как объявлять внешние символы.

  • Использование неэкспортированных внутренних имен во внешнем коде:

    Скомпилированный код должен экспортировать любые символы, на которые ссылается нескомпилированный код. Advanced Compilation and Externs описывает, как экспортировать символы.

  • Использование строковых имен для ссылки на свойства объекта:

    Компилятор переименовывает свойства в расширенном режиме, но никогда не переименовывает строки.

      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");
    

    Перед преобразованием this внутри foo.bar ссылается на foo . После преобразования this ссылается на глобальный this . В подобных случаях компилятор выдает следующее предупреждение:

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

    Чтобы сведение свойств не нарушало ваши ссылки на this , используйте this только в конструкторах и методах прототипа. Смысл this однозначен, когда вы вызываете конструктор с ключевым словом new или внутри функции, которая является свойством prototype .

  • Использование статических методов, не зная, в каком классе они вызываются:

    Например, если у вас есть:

    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:

    Компилятор плохо понимает эти конструкции. Геттеры и сеттеры ES6 преобразуются в 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 не имеет побочных эффектов, поэтому он был удален.