Entender as restrições impostas pelo Closure Compiler

O Closure Compiler espera que a entrada JavaScript esteja de acordo com algumas restrições. Quanto maior for o nível de otimização que você pedir ao compilador, mais restrições ele vai colocar no JavaScript de entrada.

Este documento descreve as principais restrições de cada nível de otimização. Consulte também esta página da wiki para conferir outras proposições feitas pelo compilador.

Restrições para todos os níveis de otimização

O compilador impõe as duas restrições a seguir em todo o JavaScript que processa, para todos os níveis de otimização:

  • O compilador reconhece apenas ECMAScript.

    O ECMAScript 5 é a versão do JavaScript compatível em quase todos os lugares. No entanto, o compilador também é compatível com muitos dos recursos do ECMAScript 6. O compilador só é compatível com recursos oficiais da linguagem.

    Recursos específicos do navegador que estão em conformidade com a especificação de linguagem ECMAScript adequada vão funcionar bem com o compilador. Por exemplo, os objetos ActiveX são criados com uma sintaxe JavaScript válida. Portanto, o código que cria objetos ActiveX funciona com o compilador.

    Os mantenedores do compilador trabalham ativamente para oferecer suporte a novas versões de linguagem e recursos. Os projetos podem especificar qual versão da linguagem ECMAScript pretendem usar com a flag --language_in.

  • O compilador não preserva comentários.

    Todos os níveis de otimização do compilador removem comentários. Portanto, o código que depende de comentários formatados de maneira especial não funciona com o compilador.

    Por exemplo, como o compilador não preserva comentários, não é possível usar os "comentários condicionais" do JScript diretamente. No entanto, é possível contornar essa restrição envolvendo comentários condicionais em expressões eval(). O compilador pode processar o seguinte código sem gerar um erro:

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

    Observação:é possível incluir licenças de código aberto e outros textos importantes na parte de cima da saída do compilador usando a anotação @preserve.

Restrições para SIMPLE_OPTIMIZATIONS

O nível de otimização simples renomeia parâmetros de função, variáveis locais e funções definidas localmente para reduzir o tamanho do código. No entanto, algumas construções de JavaScript podem interromper esse processo.

Evite as seguintes construções e práticas ao usar SIMPLE_OPTIMIZATIONS:

  • with:

    Quando você usa with, o compilador não consegue distinguir entre uma variável local e uma propriedade de objeto com o mesmo nome, e, portanto, renomeia todas as instâncias do nome.

    Além disso, a instrução with dificulta a leitura do código para humanos. A instrução with muda as regras normais de resolução de nomes e pode dificultar a identificação do que um nome se refere, mesmo para o programador que escreveu o código.

  • eval():

    O compilador não analisa o argumento de string de eval() e, portanto, não renomeia nenhum símbolo dentro desse argumento.

  • Representações de string de nomes de funções ou parâmetros:

    O compilador renomeia funções e parâmetros de função, mas não muda nenhuma string no código que se refere a funções ou parâmetros por nome. Portanto, evite representar nomes de funções ou parâmetros como strings no seu código. Por exemplo, a função de biblioteca Prototype argumentNames() usa Function.toString() para recuperar os nomes dos parâmetros de uma função. Mas, embora argumentNames() possa tentar você a usar os nomes dos argumentos no código, a compilação no modo simples quebra esse tipo de referência.

Restrições para ADVANCED_OPTIMIZATIONS

O nível de compilação ADVANCED_OPTIMIZATIONS realiza as mesmas transformações que SIMPLE_OPTIMIZATIONS e também adiciona renomeação global de propriedades, variáveis e funções, eliminação de código morto e simplificação de propriedades. Essas novas transmissões colocam restrições adicionais no JavaScript de entrada. Em geral, usar os recursos dinâmicos do JavaScript impede a análise estática correta no seu código.

Implicações da renomeação de variáveis, funções e propriedades globais:

A mudança global de nome de ADVANCED_OPTIMIZATIONS torna as práticas a seguir perigosas:

  • Referências externas não declaradas:

    Para renomear variáveis, funções e propriedades globais corretamente, o compilador precisa conhecer todas as referências a essas variáveis globais. Você precisa informar ao compilador sobre símbolos definidos fora do código que está sendo compilado. Compilação avançada e externos descreve como declarar símbolos externos.

  • Usar nomes internos não exportados em código externo:

    O código compilado precisa exportar todos os símbolos a que o código não compilado se refere. Compilação avançada e externos descreve como exportar símbolos.

  • Como usar nomes de strings para se referir a propriedades de objetos:

    O compilador renomeia propriedades no modo avançado, mas nunca strings.

      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

    Se você precisar se referir a uma propriedade com uma string entre aspas, sempre use uma string entre aspas:

      var x = { 'unrenamed_property': 1 };
      x['unrenamed_property'];  // This is OK.
      if ( 'unrenamed_property' in x ) {};   // This is OK
  • Referir-se a variáveis como propriedades do objeto global:

    O compilador renomeia propriedades e variáveis de forma independente. Por exemplo, o compilador trata as duas referências a foo a seguir de maneira diferente, mesmo que sejam equivalentes:

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

    Esse código pode ser compilado como:

      var a = {};
      window.b;

    Se você precisar se referir a uma variável como propriedade do objeto global, sempre faça isso desta forma:

    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
    }
        

    O compilador não entende que initX() e initY() são chamados no loop for e, portanto, remove os dois métodos.

    Se você transmitir uma função como um parâmetro, o compilador poderá encontrar chamadas para esse parâmetro. Por exemplo, o compilador não remove a função getHello() ao compilar o código a seguir no modo avançado.

    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.
        

Implicações do nivelamento de propriedades de objetos

No modo avançado, o compilador recolhe as propriedades do objeto para preparar a redução do nome. Por exemplo, o compilador transforma isto:

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

nisto:

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

Esse achatamento de propriedades permite que a passagem de renomeação posterior renomeie de forma mais eficiente. O compilador pode substituir foo$bar por um único caractere, por exemplo.

Mas o nivelamento de propriedades também torna as seguintes práticas perigosas:

  • Como usar this fora de construtores e métodos de protótipo:

    O nivelamento de propriedades pode mudar o significado da palavra-chave this em uma função. Exemplo:

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

    se torna:

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

    Antes da transformação, o this em foo.bar se refere a foo. Após a transformação, this se refere ao this global. Em casos como este, o compilador gera este aviso:

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

    Para evitar que o nivelamento de propriedades quebre suas referências a this, use this apenas em construtores e métodos de protótipo. O significado de this é inequívoco quando você chama um construtor com a palavra-chave new ou em uma função que é uma propriedade de um prototype.

  • Usar métodos estáticos sem saber em qual classe eles são chamados:

    Por exemplo, se você tiver:

    class A { static create() { return new A(); }};
    class B { static create() { return new B(); }};
    let cls = someCondition ? A : B;
    cls.create();
    o compilador vai recolher os dois métodos create (depois de transcompilar de ES6 para ES5), então a chamada cls.create() vai falhar. Você pode evitar isso com a anotação @nocollapse:
    class A {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    class B {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
  • Como usar "super" em um método estático sem conhecer a superclasse:

    O código a seguir é seguro porque o compilador sabe que super.sayHi() se refere a Parent.sayHi():

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

    No entanto, o nivelamento de propriedades vai interromper o código a seguir, mesmo que myMixin(Parent).sayHi seja igual a Parent.sayHi não compilado:

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

    Evite essa falha com a anotação /** @nocollapse */.

  • Usando Object.defineProperties ou getters/setters do ES6:

    O compilador não entende bem essas construções. Os getters e setters do ES6 são transformados em Object.defineProperties(...) por transpilagem. No momento, o compilador não consegue analisar estaticamente essa construção e pressupõe que o acesso e a definição de propriedades não têm efeitos colaterais. Isso pode ter repercussões perigosas. Exemplo:

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

    É compilado para:

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

    Foi determinado que C.someProperty não tinha efeitos colaterais, então ele foi removido.