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

Компилятор замыканий ожидает, что входной 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 делает опасными следующие практики:

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

    Для корректного переименования глобальных переменных, функций и свойств компилятор должен знать обо всех ссылках на эти глобальные переменные. Необходимо сообщить компилятору о символах, определённых вне компилируемого кода. В разделе «Расширенная компиляция и внешние переменные» описывается, как объявлять внешние символы.

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

    Скомпилированный код должен экспортировать все символы, на которые ссылается нескомпилированный код. В разделе «Расширенная компиляция и внешние объекты» описывается, как экспортировать символы.

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

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

      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 не имеет побочных эффектов, поэтому он был удален.