Memahami Pembatasan yang Diterapkan oleh Closure Compiler

Closure Compiler mengharapkan input JavaScript-nya sesuai dengan beberapa batasan. Makin tinggi tingkat pengoptimalan yang Anda minta agar dilakukan oleh Kompiler, makin banyak batasan yang diterapkan Kompiler pada JavaScript input.

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

Batasan untuk semua tingkat pengoptimalan

Compiler menerapkan dua batasan 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 tepat akan berfungsi dengan baik dengan compiler. Misalnya, objek ActiveX dibuat dengan sintaksis JavaScript yang valid, sehingga kode yang membuat objek ActiveX berfungsi dengan compiler.

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

  • Compiler tidak mempertahankan komentar.

    Semua tingkat 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 batasan ini dengan membungkus 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.

Batasan 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 Compiler mengganti nama semua instance nama tersebut.

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

  • eval():

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

  • Representasi string dari nama fungsi atau 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, Anda harus menghindari merepresentasikan 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 membuat Anda menggunakan nama argumen dalam kode, kompilasi mode Sederhana akan merusak jenis referensi ini.

Batasan untuk ADVANCED_OPTIMIZATIONS

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

Implikasi penggantian nama variabel, fungsi, dan properti global:

Penggantian nama global ADVANCED_OPTIMIZATIONS membuat praktik berikut menjadi berbahaya:

  • Referensi eksternal yang tidak dinyatakan:

    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 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. Kompilasi Lanjutan dan Eksternal menjelaskan cara mengekspor simbol.

  • Menggunakan nama string untuk merujuk ke properti objek:

    Kompilator 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 diapit tanda petik, selalu gunakan string yang diapit tanda petik:

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

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

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

    Kode ini dapat dikompilasi menjadi:

      var a = {};
      window.b;

    Jika Anda perlu merujuk ke variabel sebagai properti objek global, selalu rujuk dengan cara 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 Compiler 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 akan menciutkan properti objek untuk mempersiapkan pemendekan 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");

Perataan properti ini memungkinkan langkah penggantian nama selanjutnya mengganti nama secara lebih efisien. Compiler dapat mengganti foo$bar dengan satu karakter, misalnya.

Namun, perataan properti juga membuat praktik berikut menjadi 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 merujuk ke foo. Setelah transformasi, this mengacu pada this global. Dalam kasus seperti ini, Compiler akan menghasilkan peringatan berikut:

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

    Untuk mencegah perataan properti merusak referensi Anda ke this, gunakan this hanya dalam konstruktor dan metode prototipe. Makna this tidak ambigu saat Anda memanggil konstruktor dengan kata kunci new, atau dalam fungsi yang merupakan properti prototype.

  • Menggunakan metode statis tanpa mengetahui class mana yang memanggilnya:

    Misalnya, 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 ditranspilasi dari ES6 ke ES5) sehingga panggilan cls.create() akan gagal. Anda dapat menghindarinya 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 mengetahui 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, perataan properti akan merusak kode berikut, meskipun myMixin(Parent).sayHi sama dengan Parent.sayHi yang belum 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 Object.defineProperties atau getter/setter ES6:

    Compiler tidak memahami konstruksi ini dengan baik. Pengambil dan penyetel ES6 diubah menjadi Object.defineProperties(...) melalui transpilation. Saat ini, compiler tidak dapat menganalisis konstruksi ini secara statis dan mengasumsikan bahwa akses dan set properti tidak memiliki efek samping. Hal ini dapat menimbulkan dampak berbahaya. Contoh:

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

    Dikompilasi ke:

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