Memahami Batasan yang Berlaku oleh Closure Compiler

Closure Compiler mengharapkan input JavaScript-nya mematuhi beberapa batasan. Semakin tinggi tingkat pengoptimalan yang harus dilakukan Compiler, semakin banyak batasan yang ditetapkan Compiler pada JavaScript input.

Dokumen ini menjelaskan batasan utama untuk setiap tingkat pengoptimalan. Lihat juga halaman wiki ini untuk asumsi tambahan yang dibuat oleh compiler.

Pembatasan untuk semua tingkat pengoptimalan

Compiler menempatkan dua pembatasan berikut pada semua JavaScript yang diprosesnya, untuk semua tingkat pengoptimalan:

  • Compiler hanya mengenali ECMAScript.

    ECMAScript 5 adalah versi JavaScript yang didukung hampir di mana saja. Namun, compiler juga mendukung banyak fitur di ECMAScript 6. Compiler hanya mendukung fitur bahasa resmi.

    Fitur khusus browser yang sesuai dengan spesifikasi bahasa ECMAScript yang sesuai akan berfungsi baik dengan compiler. Misalnya, objek ActiveX dibuat dengan sintaksis JavaScript legal, sehingga kode yang membuat objek ActiveX berfungsi dengan compiler.

    Pengelola compiler secara aktif bekerja untuk mendukung versi bahasa baru dan fiturnya. Project dapat menentukan versi bahasa ECMAScript yang diinginkan menggunakan flag --language_in.

  • Compiler tidak menyimpan komentar.

    Semua level pengoptimalan Compiler menghapus komentar, sehingga kode yang mengandalkan komentar yang diformat secara khusus tidak berfungsi dengan Compiler.

    Misalnya, karena Compiler tidak mempertahankan komentar, Anda tidak dapat menggunakan "komentar bersyarat" JScript secara langsung. Namun, Anda dapat mengatasi pembatasan ini dengan menggabungkan komentar bersyarat dalam ekspresi eval(). Compiler dapat memproses kode berikut tanpa menghasilkan error:

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

    Catatan: Anda dapat menyertakan lisensi open source dan teks penting lainnya di bagian atas output Compiler menggunakan anotasi @preserve.

Pembatasan untuk SIMPLE_OPTIMIZATIONS

Tingkat Pengoptimalan sederhana mengganti nama parameter fungsi, variabel lokal, dan fungsi yang ditentukan secara lokal untuk mengurangi ukuran kode. Namun, beberapa konstruksi JavaScript dapat merusak proses penggantian nama ini.

Hindari konstruksi dan praktik berikut saat menggunakan SIMPLE_OPTIMIZATIONS:

  • with:

    Saat Anda menggunakan with, Compiler tidak dapat membedakan antara variabel lokal dan properti objek dengan nama yang sama, sehingga dapat mengganti nama semua instance nama.

    Selain itu, pernyataan with membuat kode Anda lebih sulit dibaca manusia. Pernyataan with mengubah aturan normal untuk resolusi nama, dan dapat menyulitkan bahkan bagi programmer yang menulis kode untuk mengidentifikasi arti nama.

  • eval():

    Compiler tidak mengurai argumen string eval(), sehingga tidak akan mengganti nama simbol apa pun dalam argumen ini.

  • Representasi string dari fungsi atau nama parameter:

    Compiler mengganti nama fungsi dan parameter fungsi, tetapi tidak mengubah string apa pun dalam kode Anda yang merujuk ke fungsi atau parameter berdasarkan nama. Oleh karena itu, hindari nama fungsi atau parameter sebagai string dalam kode Anda. Misalnya, fungsi library Prototype argumentNames() menggunakan Function.toString() untuk mengambil nama parameter fungsi. Namun, meskipun argumentNames() mungkin mendorong Anda untuk menggunakan nama argumen dalam kode, kompilasi Mode sederhana akan merusak referensi semacam ini.

Pembatasan untuk ADVanceD_OPTIMIZATIONS

Tingkat kompilasi ADVANCED_OPTIMIZATIONS menjalankan transformasi yang sama seperti SIMPLE_OPTIMIZATIONS, dan juga menambahkan penggantian nama global untuk properti, variabel, dan fungsi, penghapusan kode mati, dan perataan properti. Proses baru ini menerapkan batasan tambahan pada JavaScript input. Secara umum, penggunaan fitur dinamis JavaScript akan mencegah analisis statis yang benar pada kode Anda.

Implikasi penggantian variabel, fungsi, dan properti global:

Penggantian nama global ADVANCED_OPTIMIZATIONS membuat praktik berikut berbahaya:

  • Referensi eksternal yang tidak dideklarasikan:

    Untuk mengganti nama variabel, fungsi, dan properti global dengan benar, Compiler harus mengetahui semua referensi ke global tersebut. Anda harus memberi tahu Compiler tentang simbol yang ditentukan di luar kode yang sedang dikompilasi. Kompilasi Lanjutan dan Externs menjelaskan cara mendeklarasikan simbol eksternal.

  • Menggunakan nama internal yang tidak diekspor dalam kode eksternal:

    Kode yang dikompilasi harus mengekspor simbol apa pun yang dirujuk oleh kode yang tidak dikompilasi. Advanced Kompilasiation and Externs menjelaskan cara mengekspor simbol.

  • Menggunakan nama string untuk merujuk ke properti objek:

    Compiler mengganti nama properti dalam mode Lanjutan, tetapi tidak pernah mengganti nama string.

      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
    

    Jika Anda perlu merujuk ke properti dengan string yang dikutip, selalu gunakan string yang dikutip:

      var x = { 'unrenamed_property': 1 };
      x['unrenamed_property'];  // This is OK.
      if ( 'unrenamed_property' in x ) {};   // This is OK
    
  • Merujuk ke variabel sebagai properti objek global:

    Compiler mengganti nama properti dan variabel secara independen. Misalnya, Compiler memperlakukan dua referensi berikut untuk foo secara berbeda, meskipun keduanya setara:

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

    Kode ini mungkin dikompilasi menjadi:

      var a = {};
      window.b;
    

    Jika Anda perlu merujuk ke variabel sebagai properti objek global, selalu lihat variabel tersebut:

    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
    }
        

    Compiler tidak memahami bahwa initX() dan initY() dipanggil dalam loop for, sehingga akan menghapus kedua metode ini.

    Perhatikan bahwa jika Anda meneruskan fungsi sebagai parameter, Compiler dapat menemukan panggilan ke parameter tersebut. Misalnya, Compiler tidak menghapus fungsi getHello() saat mengompilasi kode berikut dalam mode Lanjutan.

    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.
        

Implikasi perataan properti objek

Dalam mode Lanjutan, Compiler menciutkan properti objek untuk mempersiapkan penyingkatan nama. Misalnya, Compiler mengubah ini:

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

menjadi ini:

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

Dengan meratakan properti ini, penggantian nama kartu di masa mendatang dapat dilakukan dengan lebih efisien. Misalnya, Compiler dapat mengganti foo$bar dengan satu karakter.

Namun, meratakan properti juga membuat praktik berikut berbahaya:

  • Menggunakan this di luar konstruktor dan metode prototipe:

    Perataan properti dapat mengubah arti kata kunci this dalam suatu fungsi. Contoh:

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

    menjadi:

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

    Sebelum transformasi, this dalam foo.bar mengacu pada foo. Setelah transformasi, this merujuk ke this global. Dalam kasus seperti ini, Compiler menghasilkan peringatan ini:

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

    Agar properti tidak merata agar tidak merusak referensi Anda ke this, hanya gunakan this dalam konstruktor dan metode prototipe. Arti this tidak ambigu saat Anda memanggil konstruktor dengan kata kunci new, atau dalam fungsi yang merupakan properti dari prototype.

  • Menggunakan metode statis tanpa mengetahui class mana yang memanggilnya:

    Misalnya, jika Anda memiliki:

    class A { static create() { return new A(); }};
    class B { static create() { return new B(); }};
    let cls = someCondition ? A : B;
    cls.create();
    
    compiler akan menciutkan kedua metode create (setelah melakukan transpilasi dari ES6 ke ES5) sehingga panggilan cls.create() akan gagal. Anda dapat menghindari hal ini dengan anotasi @nocollapse :
    class A {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    class B {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    

  • Menggunakan super dalam metode statis tanpa mengetahui superclass:

    Kode berikut aman karena compiler tahu bahwa super.sayHi() merujuk ke Parent.sayHi():

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

    Namun, meratakan properti akan merusak kode berikut, meskipun myMixin(Parent).sayHi sama dengan Parent.sayHi yang tidak dikompilasi:

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

    Hindari kerusakan ini dengan anotasi /** @nocollapse */.

  • Menggunakan pengambil/penyetel Object.defineProperties atau ES6:

    Compiler tidak memahami konstruksi ini dengan baik. Pengambil dan penyetel ES6 diubah menjadi Object.defineProperties(...) melalui transpilasi. Saat ini, compiler tidak dapat menganalisis konstruksi ini secara statis dan mengasumsikan akses serta penetapan properti bebas efek samping. Ini dapat menimbulkan dampak yang berbahaya. Contoh:

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

    Dikompilasi menjadi:

    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 dianggap tidak memiliki efek samping sehingga dihapus.