Omówienie ograniczeń nałożonych przez kompilatora zamknięcia

Kompilacja Closure Compiler oczekuje, że dane wejściowe JavaScriptu będą zgodne z kilkoma ograniczeniami. Im wyższy poziom optymalizacji, w jakim oczekujesz kompilatora, tym więcej ograniczeń wprowadza on w danych wejściowych JavaScript.

W tym dokumencie opisano główne ograniczenia obowiązujące na każdym poziomie optymalizacji. Dodatkowe założenia przyjęte przez kompilatora znajdziesz też na tej stronie wiki.

Ograniczenia na wszystkich poziomach optymalizacji

Kompilator nakłada następujące 2 ograniczenia dotyczące całego kodu JavaScriptu, który jest przetwarzany, na wszystkich poziomach optymalizacji:

  • Kompilator rozpoznaje tylko ECMAScript.

    ECMAScript 5 to wersja JavaScript obsługiwana prawie wszędzie. Kompilator obsługuje jednak wiele funkcji dostępnych w ECMAScript 6. Kompilator obsługuje tylko oficjalne funkcje językowe.

    Funkcje właściwe dla przeglądarki, które są zgodne z odpowiednią specyfikacją języka ECMAScript, będą działać z kompilatorem. Na przykład obiekty ActiveX są tworzone z użyciem legalnej składni JavaScript, więc kod tworzący obiekty ActiveX działa z kompilatorem.

    Konstruktorzy aktywnie pomagają w obsłudze nowych wersji językowych i funkcji. Projekty mogą określać wersję językową ECMAScript, używając flagi --language_in.

  • Compiler nie zachowuje komentarzy.

    Wszystkie poziomy optymalizacji kompilatora usuwają komentarze, więc kod oparty na specjalnie sformatowanych komentarzach nie działa w kompilatorze.

    Ponieważ kompilator nie zachowuje komentarzy, nie można bezpośrednio korzystać z „komentarzy warunkowych” JScript. Możesz jednak obejść to ograniczenie, dodając komentarze warunkowe do wyrażeń eval(). Kompilator może przetworzyć ten kod bez generowania błędu:

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

    Uwaga: u góry wyników kompilatora możesz uwzględnić licencje open source i inny ważny tekst za pomocą adnotacji @preserve.

Ograniczenia dotyczące SIMPLE_OPTIMIZATIONS

Prosty poziom optymalizacji zmienia parametry funkcji, zmienne lokalne i funkcje zdefiniowane lokalnie, aby zmniejszyć rozmiar kodu. Jednak niektóre konstrukcje JavaScriptu mogą ten proces zmienić.

Jeśli używasz SIMPLE_OPTIMIZATIONS, unikaj tych konstrukcji i praktyk:

  • with:

    Gdy używasz metody with, kompilator nie rozróżnia zmiennej lokalnej i właściwości obiektu o tej samej nazwie, więc zmienia nazwę wszystkich wystąpień tej nazwy.

    Ponadto instrukcja with utrudnia użytkownikom czytanie kodu. Instrukcja with zmienia normalne reguły rozpoznawania nazw i może utrudniać programistę, która napisała kod, aby zidentyfikować nazwę.

  • eval():

    Kompilator nie przeanalizuje argumentu ciągu znaków eval(), więc nie zmieni nazw żadnych symboli w tym argumencie.

  • Ciągi reprezentujące nazwy funkcji lub parametrów:

    Kompilator zmienił nazwy funkcji i parametrów funkcji, ale nie zmienia żadnych ciągów znaków w kodzie, które odwołują się do funkcji lub parametrów według nazwy. Dlatego w kodzie nie należy reprezentować nazw funkcji ani parametrów jako ciągów znaków. Na przykład funkcja biblioteki Prototype argumentNames() używa Function.toString() do pobrania nazw parametrów funkcji. Choć argumentNames() może wydawać się kuszący, aby użyć w kodzie nazw argumentów, to w kompilacji w trybie prostym takie pliki są tego rodzaju.

Ograniczenia dla ADVANCED_OPTIMIZATIONS

Poziom kompilacji ADVANCED_OPTIMIZATIONS jest taki sam jak przekształcenie SIMPLE_OPTIMIZATIONS, a także powoduje globalne zmiany nazw usług, zmiennych i funkcji, eliminację martwego kodu i spłaszczenie usługi. Te nowe karty nakładają dodatkowe ograniczenia na wejściowy kod JavaScript. Ogólnie rzecz biorąc, używanie dynamicznych funkcji JavaScriptu uniemożliwia prawidłowe analizowanie statycznego kodu.

Konsekwencje zmiany zmiennej globalnej, funkcji i właściwości:

Globalna zmiana nazwy usługi ADVANCED_OPTIMIZATIONS sprawia, że następujące praktyki są niebezpieczne:

  • Niezadeklarowane pliki referencyjne:

    Aby nazwy i funkcje globalne oraz ich funkcje były prawidłowe, kompilator musi znać wszystkie odwołania do tych zmiennych globalnych. Musisz poinformować kompilator o symbolach zdefiniowanych poza skompilowanym kodem. Zbiór zaawansowanych i zaawansowanych opisuje, jak deklarować symbole zewnętrzne.

  • Używanie niewyeksportowanych nazw wewnętrznych w kodzie zewnętrznym:

    Skompilowany kod musi wyeksportować wszystkie symbole, których dotyczy nieskompilowany kod. Zaawansowane kompilacje i testy opisuje sposób eksportowania symboli.

  • Odwoływanie się do właściwości obiektu przy użyciu nazw ciągów:

    Kompilator zmienia nazwy właściwości w trybie zaawansowanym, ale nigdy nie zmienia nazw ciągów.

      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
    

    Jeśli musisz odwoływać się do właściwości z ciągiem cytowanym, zawsze używaj tego ciągu:

      var x = { 'unrenamed_property': 1 };
      x['unrenamed_property'];  // This is OK.
      if ( 'unrenamed_property' in x ) {};   // This is OK
    
  • Odwoływanie się do zmiennych jako właściwości obiektu globalnego:

    Kompilator niezależnie zmienia nazwy właściwości i zmiennych. Na przykład kompilator traktuje poniższe 2 odwołania do zmiennej foo, choć są one równoważne:

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

    Ten kod może się skompilować, aby:

      var a = {};
      window.b;
    

    Jeśli jako wartość obiektu globalnego musisz odwoływać się do zmiennej, zawsze używaj jej w ten sposó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
    }
        

    Kompilator nie rozumie, że wywołania initX() i initY() są wywoływane w pętli for, dlatego obie te metody zostaną usunięte.

    Pamiętaj, że jeśli przekażesz funkcję jako parametr, kompilator może znaleźć wywołania tego parametru. Na przykład kompilator nie usuwa funkcji getHello() podczas kompilowania poniższego kodu w trybie zaawansowanym.

    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.
        

Konsekwencje spłaszczenia obiektu

W trybie zaawansowanym kompilator zwija właściwości obiektu, aby przygotować się do skrócenia nazwy. Na przykład kompilator przekształca to:

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

w:

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

Ta zmiana właściwości umożliwia późniejszą zmianę nazwy karty. Kompilator może zastąpić foo$bar jednym znakiem.

Jednak takie rozwiązanie zmniejsza też ryzyko:

  • Używanie metody this poza konstruktorami i prototypami:

    Scalanie właściwości może zmieniać znaczenie słowa kluczowego this w funkcji. Przykład:

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

    zmienia się w:

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

    Przed przekształceniem element this w obrębie foo.bar odnosi się do foo. Po przekształceniu this odnosi się do globalnego elementu this. W takich przypadkach kompilator generuje takie ostrzeżenie:

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

    Aby zapobiec ograniczeniu rozprzestrzeniania się właściwości w odniesieniu do obiektu this, używaj właściwości this tylko w konstruktorze i prototypach. Znaczenie wartości this jest jednoznaczne z wywołaniem konstruktora ze słowem kluczowym new lub w ramach funkcji, która jest właściwością prototype.

  • Za pomocą metod statycznych bez wiedzy klasy, do której zostały wywołane:

    Jeśli masz na przykład:

    class A { static create() { return new A(); }};
    class B { static create() { return new B(); }};
    let cls = someCondition ? A : B;
    cls.create();
    
    Kompilator zwinie obie metody create (po wykonaniu transpilacji z ES6 do ES5), a wywołanie cls.create() się nie powiedzie. Możesz tego uniknąć, korzystając z adnotacji @nocollapse:
    class A {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    class B {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    

  • Używanie superwersji w metodzie statycznej bez znajomości klasy:

    Ten kod jest bezpieczny, ponieważ kompilator wie, że super.sayHi() odnosi się do Parent.sayHi():

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

    Rozbudowywanie właściwości spowoduje jednak uszkodzenie poniższego kodu, nawet jeśli myMixin(Parent).sayHi ma wartość Parent.sayHi nieskompilowaną:

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

    Aby uniknąć tych przerw, dodaj adnotację /** @nocollapse */.

  • Przy użyciu obiektu Object.defineProperties lub get ES6:

    Kompilator nie do końca rozumie te konstrukcje. Metoda pobierania i ustawiania ES6 jest przekształcana w obiekt Object.defineProperties(...) przez transpilację. Obecnie kompilator nie może statycznie analizować tego konstrukcji i przyjmuje, że właściwości i ustawione właściwości są efekty uboczne. Może to wiązać się z niebezpiecznymi konsekwencjami. Przykład:

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

    Kompilacja:

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

    Uznano, że usługa C.someProperty nie ma żadnych efektów ubocznych, dlatego została usunięta.