Hiểu rõ các quy định hạn chế do Trình biên dịch đóng

Trình biên dịch đóng này yêu cầu dữ liệu đầu vào JavaScript phải tuân thủ một số hạn chế. Bạn càng yêu cầu trình biên dịch thực hiện càng cao, mức độ tối ưu hoá mà Trình biên dịch thực hiện càng hạn chế 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 mức độ tối ưu hoá. Hãy xem thêm trang wiki này để biết các giả định khác do trình biên dịch thực hiện.

Các hạn chế đối với tất cả các cấp tối ưu hóa

Trình biên dịch đặt hai quy định hạn chế sau đây đối với tất cả JavaScript mà trình biên dịch xử lý, đối với 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 theo quy cách ngôn ngữ phù hợp của ECMAScript sẽ hoạt động tốt với trình biên dịch. Ví dụ: các đối tượngKotlin được tạo bằng cú pháp JavaScript pháp lý, vì vậy, mã tạo đối tượng ONIX sẽ hoạt động với trình biên dịch.

    Trình 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à dự định sử dụng bằng cách sử dụng cờ --language_in.

  • Trình biên dịch không giữ lại nhận xét.

    Tất cả các cấp tối ưu hoá của Trình biên dịch đều xoá các nhận xét, vì vậy, mã dựa trên nhận xét được định dạng đặc biệt 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 nhận xét, nên bạn không thể sử dụng trực tiếp "các nhận xét 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 gói nhận xét 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 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 ra của Trình biên dịch bằng cách sử dụng chú thích @pre gì.

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

Cấp tối ưu hoá đơn giản sẽ đổi tên các thông 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ể phá vỡ quá trình đổi tên này.

Hã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 biến cục bộ và 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 bản sao của tên.

    Ngoài ra, câu lệnh with còn khiến mã của bạn khó đọc 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 cho ngay cả lập trình viên đã viết mã để xác định nội dung của tên.

  • 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ỳ ký hiệu nào trong đối số này.

  • Chuỗi đại diện của tên hàm hoặc 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ã 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 argumentNames() trong thư viện nguyên mẫu sử dụng Function.toString() để truy xuất tên của các tham số của một hàm. Tuy nhiên, mặc dù argumentNames() có thể muốn bạn sử dụng tên của các đối số trong mã, nhưng quá trình biên dịch ở chế độ Đơn giản sẽ làm hỏng loại tham chiếu này.

Quy định hạn chế đối với Elevate_ hoạt động

Cấp độ biên dịch ADVANCED_OPTIMIZATIONS thực hiện các biến đổi tương tự như SIMPLE_OPTIMIZATIONS, đồng thời thêm tên đổi mới toàn bộ các thuộc tính, biến và hàm, loại bỏ mã chết và làm phẳng thuộc tính. Các lượt truyền mới này đặt ra các hạn chế bổ sung cho 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 phân tích tĩnh chính xác trên mã của bạn.

Gợi ý các biến toàn cục, hàm và thuộc tính:

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

  • Tài liệu tham khảo bên ngoài không được khai báo:

    Để đổi tên chính xác các biến, hàm và thuộc tính toàn cục, Trình biên dịch phải biết về tất cả các tham chiếu đến các tập hợp toàn cục đó. Bạn phải cho Trình biên dịch biết về các ký hiệu được xác định bên ngoài mã đang được biên dịch. Biên dịch nâng cao và 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. Biên dịch nâng cao và 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 trong 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 bạn cần tham chiếu đến một thuộc tính có chuỗi trích dẫn, luôn sử dụng chuỗi được trích dẫn:

      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ý hai tham chiếu sau đến foo khác nhau, mặc dù hai tham chiếu này 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 toàn cục, 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, vì vậy 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 ở dạng tham số, trình biên dịch sẽ có thể tìm 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.
        

Ngụ ý về việc làm phẳng thuộc tính đối tượng

Ở Chế độ nâng cao, Trình biên dịch thu gọn các thuộc tính đối tượng để chuẩn bị rút ngắn tên. Ví dụ: Trình biên dịch chuyển đổi đoạn mã sau:

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

vào:

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

Chế độ phân tách thuộc tính này cho phép việc đổi tên sau này đổi tên hiệu quả hơn. Ví dụ: Trình biên dịch có thể thay thế foo$bar bằng một ký tự duy nhất.

Tuy nhiên, việc làm phẳng thuộc tính cũng khiến các phương pháp sau đây 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:

    Chế độ làm phẳng 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 tham chiếu đến this toàn cục. Trong trường hợp như thế này, Trình biên dịch sẽ tạo ra cảnh báo này:

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

    Để ngăn tình trạng làm phẳng thuộc tính không làm hỏng các tệp tham chiếu của bạn đến this, chỉ sử dụng this trong các hàm dựng và phương thức nguyên mẫu. Ý nghĩa của this là không rõ ràng khi bạn gọi một hàm khởi tạo có 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 lớp được gọi:

    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) để lệnh gọi cls.create() không thành công. Bạn có thể tránh điều này bằng chú thích @nocollapse:
    class A {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    class B {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    

  • Sử dụng lớp cao cấp trong phương thức tĩnh mà không biết lớp cao cấp:

    Mã sau đây là an toàn vì trình biên dịch biết rằng super.sayHi() tham chiếu đế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ẽ phá vỡ mã sau, ngay cả khi myMixin(Parent).sayHi bằng Parent.sayHi không được biên dịch:

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

    Hãy tránh chú thích này bằng chú thích /** @nocollapse */.

  • Sử dụng Object.verifyProperties hoặc getter/setter ES6:

    Trình biên dịch không hiểu rõ các cấu trúc này. Phương thức getter và setter ES6 được chuyển đổi thành Object.màPropertiesProperties(...) thông qua quá trình chuyển đổi. 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 các thuộc tính truy cập và thiết lập không có tác dụng phụ. Điều này có thể dẫn đến các 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á.