Tìm hiểu các quy định hạn chế do Closure Compiler áp đặt

Trình biên dịch Closure yêu cầu đầu vào JavaScript phải tuân thủ một số hạn chế. Bạn yêu cầu Trình biên dịch thực hiện mức tối ưu hoá càng cao thì Trình biên dịch càng đặt nhiều hạn chế đối với JavaScript đầu vào.

Tài liệu này mô tả các hạn chế chính đối với từng cấp độ tối ưu hoá. Xem thêm trang wiki này để biết thêm các giả định mà trình biên dịch đưa ra.

Các quy định hạn chế đối với tất cả các cấp tối ưu hoá

Trình biên dịch đặt 2 hạn chế sau đây đối với tất cả JavaScript mà trình biên dịch xử lý, cho tất cả các cấp độ tối ưu hoá:

  • Trình biên dịch chỉ nhận dạng ECMAScript.

    ECMAScript 5 là phiên bản JavaScript được hỗ trợ ở hầu hết mọi nơi. Tuy nhiên, trình biên dịch cũng hỗ trợ nhiều tính năng trong ECMAScript 6. Trình biên dịch chỉ hỗ trợ các tính năng ngôn ngữ chính thức.

    Các tính năng dành riêng cho trình duyệt tuân thủ quy cách ngôn ngữ ECMAScript thích hợp sẽ hoạt động bình thường với trình biên dịch. Ví dụ: các đối tượng ActiveX được tạo bằng cú pháp JavaScript hợp lệ, vì vậy mã tạo các đối tượng ActiveX sẽ hoạt động với trình biên dịch.

    Các người duy trì trình biên dịch tích cực làm việc để hỗ trợ các phiên bản ngôn ngữ mới và các tính năng của chúng. Các dự án có thể chỉ định phiên bản ngôn ngữ ECMAScript mà chúng dự định sử dụng bằng cách dùng cờ --language_in.

  • Trình biên dịch không giữ lại các bình luận.

    Tất cả các cấp tối ưu hoá Trình biên dịch đều xoá phần nhận xét, vì vậy, mã dựa vào phần nhận xét có định dạng đặc biệt sẽ không hoạt động với Trình biên dịch.

    Ví dụ: vì Trình biên dịch không giữ lại chú thích, nên bạn không thể sử dụng trực tiếp "chú thích có điều kiện" của JScript. Tuy nhiên, bạn có thể giải quyết hạn chế này bằng cách bao bọc các bình luận có điều kiện trong biểu thức eval(). Trình biên dịch có thể xử lý mã sau mà không tạo ra lỗi:

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

    Lưu ý: Bạn có thể thêm giấy phép nguồn mở và văn bản quan trọng khác vào đầu đầu ra của Trình biên dịch bằng cách sử dụng chú thích @preserve.

Các hạn chế đối với SIMPLE_OPTIMIZATIONS

Cấp độ tối ưu hoá Đơn giản sẽ đổi tên các tham số hàm, biến cục bộ và hàm được xác định cục bộ để giảm kích thước mã. Tuy nhiên, một số cấu trúc JavaScript có thể làm gián đoạn quy trình đổi tên này.

Tránh các cấu trúc và phương pháp sau khi sử dụng SIMPLE_OPTIMIZATIONS:

  • with:

    Khi bạn sử dụng with, Trình biên dịch không thể phân biệt giữa một biến cục bộ và một thuộc tính đối tượng có cùng tên, do đó, trình biên dịch sẽ đổi tên tất cả các thực thể của tên đó.

    Hơn nữa, câu lệnh with khiến con người khó đọc mã của bạn hơn. Câu lệnh with thay đổi các quy tắc thông thường để phân giải tên và có thể gây khó khăn ngay cả cho lập trình viên đã viết mã để xác định tên đề cập đến điều gì.

  • eval():

    Trình biên dịch không phân tích cú pháp đối số chuỗi của eval(), do đó, trình biên dịch sẽ không đổi tên bất kỳ biểu tượng nào trong đối số này.

  • Biểu thị chuỗi của tên hàm hoặc tên tham số:

    Trình biên dịch đổi tên các hàm và tham số hàm nhưng không thay đổi bất kỳ chuỗi nào trong mã của bạn tham chiếu đến các hàm hoặc tham số theo tên. Do đó, bạn nên tránh biểu thị tên hàm hoặc tham số dưới dạng chuỗi trong mã. Ví dụ: hàm thư viện Prototype argumentNames() sử dụng Function.toString() để truy xuất tên của các tham số của hàm. Nhưng trong khi argumentNames() có thể khiến bạn muốn sử dụng tên của các đối số trong mã, thì quá trình biên dịch ở chế độ Đơn giản sẽ phá vỡ loại tham chiếu này.

Các hạn chế đối với ADVANCED_OPTIMIZATIONS

Cấp độ biên dịch ADVANCED_OPTIMIZATIONS thực hiện các phép biến đổi tương tự như SIMPLE_OPTIMIZATIONS, đồng thời thêm việc đổi tên chung cho các thuộc tính, biến và hàm, loại bỏ mã không dùng và làm phẳng thuộc tính. Các lượt truyền mới này đặt thêm các quy định hạn chế đối với JavaScript đầu vào. Nhìn chung, việc sử dụng các tính năng động của JavaScript sẽ ngăn chặn việc phân tích tĩnh chính xác trên mã của bạn.

Hậu quả của việc đổi tên biến, hàm và thuộc tính trên toàn cục:

Việc đổi tên ADVANCED_OPTIMIZATIONS trên toàn cầu khiến các phương pháp sau đây trở nên nguy hiểm:

  • Tham chiếu bên ngoài chưa được khai báo:

    Để đổi tên các biến, hàm và thuộc tính chung một cách chính xác, Trình biên dịch phải biết về tất cả các tham chiếu đến các biến chung đó. Bạn phải cho Trình biên dịch biết về các biểu tượng được xác định bên ngoài mã đang được biên dịch. Quy trình biên dịch nâng cao và các tệp bên ngoài mô tả cách khai báo các biểu tượng bên ngoài.

  • Sử dụng tên nội bộ chưa xuất trong mã bên ngoài:

    Mã đã biên dịch phải xuất mọi biểu tượng mà mã chưa biên dịch tham chiếu đến. Advanced Compilation and Externs (Biên dịch nâng cao và các tệp bên ngoài) mô tả cách xuất các biểu tượng.

  • Sử dụng tên chuỗi để tham chiếu đến các thuộc tính đối tượng:

    Trình biên dịch đổi tên các thuộc tính ở chế độ Nâng cao, nhưng không bao giờ đổi tên các chuỗi.

      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

    Nếu cần tham chiếu đến một thuộc tính có chuỗi trong dấu ngoặc kép, hãy luôn sử dụng chuỗi trong dấu ngoặc kép:

      var x = { 'unrenamed_property': 1 };
      x['unrenamed_property'];  // This is OK.
      if ( 'unrenamed_property' in x ) {};   // This is OK
  • Tham chiếu đến các biến dưới dạng thuộc tính của đối tượng chung:

    Trình biên dịch đổi tên các thuộc tính và biến một cách độc lập. Ví dụ: Trình biên dịch xử lý 2 tham chiếu sau đây đến foo theo cách khác nhau, mặc dù chúng tương đương nhau:

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

    Mã này có thể biên dịch thành:

      var a = {};
      window.b;

    Nếu bạn cần tham chiếu đến một biến dưới dạng thuộc tính của đối tượng chung, hãy luôn tham chiếu đến biến đó theo cách sau:

    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
    }
        

    Trình biên dịch không hiểu rằng initX()initY() được gọi trong vòng lặp for, do đó, trình biên dịch sẽ xoá cả hai phương thức này.

    Xin lưu ý rằng nếu bạn truyền một hàm làm tham số, thì Trình biên dịch có thể tìm thấy các lệnh gọi đến tham số đó. Ví dụ: Trình biên dịch không xoá hàm getHello() khi biên dịch mã sau ở chế độ Nâng cao.

    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.
        

Hàm ý của việc làm phẳng thuộc tính đối tượng

Ở chế độ Nâng cao, Trình biên dịch sẽ thu gọn các thuộc tính đối tượng để chuẩn bị cho việc rút ngắn tên. Ví dụ: Trình biên dịch sẽ chuyển đổi nội dung này:

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

thành:

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

Việc đơn giản hoá thuộc tính này cho phép quá trình đổi tên sau này đổi tên hiệu quả hơn. Trình biên dịch có thể thay thế foo$bar bằng một ký tự duy nhất, chẳng hạn.

Tuy nhiên, việc đơn giản hoá tài sản cũng khiến những hành vi sau trở nên nguy hiểm:

  • Sử dụng this bên ngoài hàm khởi tạo và phương thức nguyên mẫu:

    Việc đơn giản hoá thuộc tính có thể thay đổi ý nghĩa của từ khoá this trong một hàm. Ví dụ:

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

    trở thành:

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

    Trước khi chuyển đổi, this trong foo.bar đề cập đến foo. Sau khi chuyển đổi, this sẽ đề cập đến this trên toàn cầu. Trong những trường hợp như thế này, Trình biên dịch sẽ đưa ra cảnh báo sau:

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

    Để ngăn việc làm phẳng thuộc tính làm hỏng các tham chiếu đến this, chỉ sử dụng this trong các hàm khởi tạo và phương thức nguyên mẫu. Ý nghĩa của this là rõ ràng khi bạn gọi một hàm khởi tạo bằng từ khoá new hoặc trong một hàm là thuộc tính của prototype.

  • Sử dụng các phương thức tĩnh mà không biết chúng được gọi trên lớp nào:

    Ví dụ: nếu bạn có:

    class A { static create() { return new A(); }};
    class B { static create() { return new B(); }};
    let cls = someCondition ? A : B;
    cls.create();
    trình biên dịch sẽ thu gọn cả hai phương thức create (sau khi chuyển đổi từ ES6 sang ES5) nên lệnh gọi cls.create() sẽ không thành công. Bạn có thể tránh điều này bằng chú giải @nocollapse:
    class A {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    class B {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
  • Sử dụng super trong một phương thức tĩnh mà không biết lớp cấp cao:

    Đoạn mã sau đây là an toàn, vì trình biên dịch biết rằng super.sayHi() đề cập đến Parent.sayHi():

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

    Tuy nhiên, việc làm phẳng thuộc tính sẽ làm hỏng đoạn mã sau, ngay cả khi myMixin(Parent).sayHi bằng Parent.sayHi chưa được biên dịch:

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

    Tránh trường hợp này bằng chú giải /** @nocollapse */.

  • Sử dụng Object.defineProperties hoặc phương thức truy cập/sửa đổi ES6:

    Trình biên dịch không hiểu rõ những cấu trúc này. Phương thức getter và setter ES6 được chuyển đổi thành Object.defineProperties(...) thông qua quá trình chuyển đổi mã nguồn. Hiện tại, trình biên dịch không thể phân tích tĩnh cấu trúc này và giả định rằng việc truy cập và thiết lập các thuộc tính không có tác dụng phụ. Điều này có thể gây ra những hậu quả nguy hiểm. Ví dụ:

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

    Được biên dịch thành:

    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 được xác định là không có tác dụng phụ nên đã bị xoá.