Informacje o ograniczeniach narzucanych przez kompilator Closure

Kompilator Closure oczekuje, że dane wejściowe JavaScript będą zgodne z kilkoma ograniczeniami. Im wyższy poziom optymalizacji, o który prosisz kompilator, tym więcej ograniczeń nakłada on na wejściowy kod JavaScript.

W tym dokumencie opisujemy główne ograniczenia dla każdego poziomu optymalizacji. Dodatkowe założenia kompilatora znajdziesz też na tej stronie wiki.

Ograniczenia na wszystkich poziomach optymalizacji

Kompilator nakłada na cały przetwarzany kod JavaScript te 2 ograniczenia na wszystkich poziomach optymalizacji:

  • Kompilator rozpoznaje tylko ECMAScript.

    ECMAScript 5 to wersja JavaScriptu obsługiwana niemal wszędzie. Komputer obsługuje jednak wiele funkcji ECMAScript 6. Kompilator obsługuje tylko funkcje języka urzędowego.

    Funkcje specyficzne dla przeglądarki, które są zgodne z odpowiednią specyfikacją języka ECMAScript, będą działać prawidłowo z kompilatorem. Na przykład obiekty ActiveX są tworzone za pomocą prawidłowej składni JavaScript, więc kod, który tworzy obiekty ActiveX, działa z kompilatorem.

    Osoby odpowiedzialne za kompilator aktywnie pracują nad obsługą nowych wersji języka i ich funkcji. Projekty mogą określać, której wersji języka ECMAScript chcą używać, za pomocą flagi --language_in.

  • Kompilator nie zachowuje komentarzy.

    Wszystkie poziomy optymalizacji kompilatora usuwają komentarze, więc kod, który opiera się na specjalnie sformatowanych komentarzach, nie działa z kompilatorem.

    Na przykład kompilator nie zachowuje komentarzy, więc nie możesz bezpośrednio używać „komentarzy warunkowych” JScript. Możesz jednak obejść to ograniczenie, umieszczając komentarze warunkowe w wyrażeniach eval(). Kompilator może przetworzyć ten kod bez generowania błędu:

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

    Uwaga: możesz umieścić licencje open source i inne ważne informacje u góry danych wyjściowych kompilatora, używając adnotacji @preserve.

Ograniczenia dotyczące SIMPLE_OPTIMIZATIONS

Poziom optymalizacji Simple zmienia nazwy parametrów funkcji, zmiennych lokalnych i funkcji zdefiniowanych lokalnie, aby zmniejszyć rozmiar kodu. Jednak niektóre konstrukcje JavaScriptu mogą przerwać ten proces zmiany nazwy.

Podczas korzystania z SIMPLE_OPTIMIZATIONS unikaj tych konstrukcji i praktyk:

  • with:

    Gdy używasz with, kompilator nie może odróżnić zmiennej lokalnej od właściwości obiektu o tej samej nazwie, więc zmienia nazwy wszystkich wystąpień.

    Ponadto instrukcja with utrudnia odczytanie kodu przez człowieka. Instrukcja with zmienia normalne reguły rozpoznawania nazw i może utrudnić nawet programiście, który napisał kod, określenie, do czego odnosi się dana nazwa.

  • eval():

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

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

    Kompilator zmienia 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 przedstawiać nazw funkcji ani parametrów jako ciągów znaków. Na przykład funkcja biblioteki Prototype argumentNames() używa Function.toString() do pobierania nazw parametrów funkcji. Chociaż argumentNames() może zachęcać do używania w kodzie nazw argumentów, kompilacja w trybie prostym przerywa tego rodzaju odwołania.

Ograniczenia dotyczące ADVANCED_OPTIMIZATIONS

Poziom kompilacji ADVANCED_OPTIMIZATIONS wykonuje te same przekształcenia co poziom SIMPLE_OPTIMIZATIONS, a dodatkowo przeprowadza globalne zmienianie nazw właściwości, zmiennych i funkcji, usuwanie martwego kodu i spłaszczanie właściwości. Te nowe etapy nakładają dodatkowe ograniczenia na wejściowy kod JavaScript. Ogólnie rzecz biorąc, korzystanie z dynamicznych funkcji JavaScriptu uniemożliwia prawidłową analizę statyczną kodu.

Konsekwencje zmiany nazw zmiennych globalnych, funkcji i właściwości:

Globalna zmiana nazwy ADVANCED_OPTIMIZATIONS sprawia, że te praktyki są niebezpieczne:

  • Niezadeklarowane odwołania do elementów zewnętrznych:

    Aby poprawnie zmienić nazwy zmiennych globalnych, funkcji i właściwości, kompilator musi znać wszystkie odwołania do tych zmiennych globalnych. Musisz poinformować kompilator o symbolach zdefiniowanych poza kompilowanym kodem. W artykule Zaawansowana kompilacja i pliki zewnętrzne opisujemy, jak deklarować symbole zewnętrzne.

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

    Skompilowany kod musi eksportować wszystkie symbole, do których odwołuje się nieskompilowany kod. W artykule Zaawansowana kompilacja i pliki zewnętrzne znajdziesz informacje o eksportowaniu symboli.

  • Używanie nazw ciągów znaków do odwoływania się do właściwości obiektów:

    Kompilator zmienia nazwy właściwości w trybie zaawansowanym, ale nigdy nie zmienia nazw ciągów znakó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łać się do właściwości z ciągiem znaków w cudzysłowie, zawsze używaj ciągu znaków w cudzysłowie:

      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 zmienia nazwy właściwości i zmiennych niezależnie od siebie. Na przykład kompilator traktuje te 2 odwołania do foo inaczej, mimo że są one równoważne:

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

    Ten kod może zostać skompilowany do:

      var a = {};
      window.b;

    Jeśli chcesz odwołać się do zmiennej jako właściwości obiektu globalnego, zawsze rób to 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 wie, że initX()initY() są wywoływane w pętli for, więc usuwa obie te metody.

    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łaszczania właściwości obiektu

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

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

w ten sposób:

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

Spłaszczenie tej właściwości umożliwia późniejsze wydajniejsze zmienianie nazw. Kompilator może na przykład zastąpić ciąg foo$bar pojedynczym znakiem.

Spłaszczanie właściwości sprawia też, że niebezpieczne stają się te praktyki:

  • Używanie this poza konstruktorami i metodami prototypu:

    Spłaszczanie właściwości może zmienić znaczenie słowa kluczowego this w funkcji. Na 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 thisfoo.bar odnosi się do foo. Po przekształceniu this odnosi się do globalnego this. W takich przypadkach kompilator wyświetla to ostrzeżenie:

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

    Aby zapobiec uszkodzeniu odwołań do this w wyniku spłaszczenia właściwości, używaj this tylko w konstruktorach i metodach prototypu. Znaczenie słowa this jest jednoznaczne, gdy wywołujesz konstruktor za pomocą słowa kluczowego new lub w funkcji, która jest właściwością obiektu prototype.

  • Używanie metod statycznych bez wiedzy o tym, w której klasie są wywoływane:

    Na przykład, jeśli masz:

    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 transpilacji z ES6 do ES5), więc wywołanie cls.create() się nie powiedzie. Możesz tego uniknąć, używając adnotacji @nocollapse:
    class A {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    class B {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
  • Używanie słowa kluczowego super w metodzie statycznej bez znajomości klasy nadrzędnej:

    Poniższy 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();

    Spłaszczanie właściwości spowoduje jednak, że ten kod przestanie działać, nawet jeśli myMixin(Parent).sayHi jest równe Parent.sayHi nieskompilowanemu:

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

    Unikaj tego problemu dzięki adnotacji /** @nocollapse */.

  • Używanie Object.defineProperties lub getterów/setterów ES6:

    Kompilator nie rozumie dobrze tych konstrukcji. Gettery i settery ES6 są przekształcane w Object.defineProperties(...) w procesie transpilacji. Obecnie kompilator nie może statycznie analizować tej konstrukcji i zakłada, że dostęp do właściwości i ich ustawianie nie powoduje efektów ubocznych. Może to mieć niebezpieczne konsekwencje. Na przykład:

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

    Jest kompilowany do:

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

    Właściwość C.someProperty nie ma efektów ubocznych, więc została usunięta.