ทำความเข้าใจข้อจำกัดที่ Closure Compiler กำหนด

Closure Compiler คาดหวังว่าอินพุต JavaScript จะเป็นไปตามข้อจำกัดบางอย่าง ยิ่งคุณขอให้คอมไพเลอร์ทำการเพิ่มประสิทธิภาพในระดับสูงเท่าใด คอมไพเลอร์ก็จะยิ่งกำหนดข้อจำกัดกับ JavaScript อินพุตมากขึ้นเท่านั้น

เอกสารนี้อธิบายข้อจํากัดหลักสําหรับการเพิ่มประสิทธิภาพแต่ละระดับ ดูสมมติฐานเพิ่มเติมที่คอมไพเลอร์ใช้ได้ในหน้าวิกิหน้านี้

ข้อจำกัดสำหรับระดับการเพิ่มประสิทธิภาพทั้งหมด

คอมไพเลอร์จะกำหนดข้อจำกัด 2 อย่างต่อไปนี้ ใน JavaScript ทั้งหมดที่ประมวลผลสำหรับระดับการเพิ่มประสิทธิภาพทั้งหมด

  • คอมไพเลอร์รู้จักเฉพาะ ECMAScript

    ECMAScript 5 เป็น JavaScript เวอร์ชันที่รองรับเกือบทุกที่ อย่างไรก็ตาม คอมไพเลอร์ยังรองรับฟีเจอร์หลายอย่างใน ECMAScript 6 ด้วย คอมไพเลอร์รองรับเฉพาะฟีเจอร์ภาษาอย่างเป็นทางการเท่านั้น

    ฟีเจอร์เฉพาะของเบราว์เซอร์ที่เป็นไปตามข้อกำหนดของภาษา ECMAScript ที่เหมาะสมจะทำงานร่วมกับคอมไพเลอร์ได้อย่างราบรื่น ตัวอย่างเช่น ออบเจ็กต์ ActiveX สร้างขึ้นด้วยไวยากรณ์ JavaScript ที่ถูกต้องตามกฎหมาย ดังนั้นโค้ด ที่สร้างออบเจ็กต์ ActiveX จึงใช้ได้กับคอมไพเลอร์

    ผู้ดูแลคอมไพเลอร์ทำงานอย่างต่อเนื่องเพื่อรองรับภาษาเวอร์ชันใหม่และฟีเจอร์ต่างๆ โปรเจ็กต์สามารถระบุเวอร์ชันภาษา ECMAScript ที่ต้องการได้โดยใช้แฟล็ก --language_in

  • คอมไพเลอร์จะไม่เก็บรักษาความคิดเห็น

    การเพิ่มประสิทธิภาพของคอมไพเลอร์ทุกระดับจะนำความคิดเห็นออก ดังนั้นโค้ดที่ อาศัยความคิดเห็นที่จัดรูปแบบเป็นพิเศษจึงใช้กับ คอมไพเลอร์ไม่ได้

    ตัวอย่างเช่น เนื่องจากคอมไพเลอร์ไม่ได้เก็บความคิดเห็นไว้ คุณจึง ใช้ "ความคิดเห็นแบบมีเงื่อนไข" ของ JScript โดยตรงไม่ได้ อย่างไรก็ตาม คุณสามารถหลีกเลี่ยงข้อจำกัดนี้ได้โดยการรวมความคิดเห็นแบบมีเงื่อนไข ไว้ในนิพจน์ eval() คอมไพเลอร์สามารถ ประมวลผลโค้ดต่อไปนี้ได้โดยไม่สร้างข้อผิดพลาด

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

    หมายเหตุ: คุณสามารถใส่ใบอนุญาตโอเพนซอร์สและ ข้อความสำคัญอื่นๆ ไว้ที่ด้านบนของเอาต์พุตคอมไพเลอร์ โดยใช้คำอธิบายประกอบ @preserve

ข้อจำกัดสำหรับ SIMPLE_OPTIMIZATIONS

ระดับการเพิ่มประสิทธิภาพอย่างง่ายจะเปลี่ยนชื่อพารามิเตอร์ฟังก์ชัน ตัวแปรภายใน และฟังก์ชันที่กำหนดไว้ภายในเพื่อลดขนาดโค้ด อย่างไรก็ตาม โครงสร้าง JavaScript บางอย่างอาจทำให้กระบวนการเปลี่ยนชื่อนี้หยุดชะงัก

หลีกเลี่ยงโครงสร้างและแนวทางต่อไปนี้เมื่อใช้ SIMPLE_OPTIMIZATIONS

  • with:

    เมื่อคุณใช้ with คอมไพเลอร์จะแยกความแตกต่าง ระหว่างตัวแปรภายในกับพร็อพเพอร์ตี้ออบเจ็กต์ที่มีชื่อเดียวกันไม่ได้ จึงเปลี่ยนชื่ออินสแตนซ์ทั้งหมดของชื่อนั้น

    นอกจากนี้ คำสั่ง with ยังทำให้โค้ดอ่านยากขึ้น สำหรับมนุษย์ คำสั่ง with จะเปลี่ยนกฎปกติสำหรับการแก้ชื่อ และอาจทำให้แม้แต่โปรแกรมเมอร์ที่เขียนโค้ดก็ระบุได้ยากว่าชื่อนั้นหมายถึงอะไร

  • eval():

    คอมไพเลอร์จะไม่แยกวิเคราะห์อาร์กิวเมนต์สตริง ของ eval() จึงจะไม่เปลี่ยนชื่อสัญลักษณ์ใดๆ ภายใน อาร์กิวเมนต์นี้

  • การแสดงชื่อฟังก์ชันหรือพารามิเตอร์เป็นสตริง:

    คอมไพเลอร์จะเปลี่ยนชื่อฟังก์ชันและพารามิเตอร์ของฟังก์ชัน แต่จะไม่ เปลี่ยนสตริงใดๆ ในโค้ดที่อ้างอิงถึงฟังก์ชันหรือ พารามิเตอร์ตามชื่อ ดังนั้นคุณจึงควรหลีกเลี่ยงการแสดงชื่อฟังก์ชันหรือ พารามิเตอร์เป็นสตริงในโค้ด ตัวอย่างเช่น ฟังก์ชันไลบรารี Prototype argumentNames() ใช้ Function.toString() เพื่อดึงชื่อของพารามิเตอร์ของฟังก์ชัน แต่ในขณะที่ argumentNames() อาจ กระตุ้นให้คุณใช้ชื่อของอาร์กิวเมนต์ในโค้ด การคอมไพล์โหมด Simple จะทําให้การอ้างอิงประเภทนี้ใช้งานไม่ได้

ข้อจำกัดสำหรับ ADVANCED_OPTIMIZATIONS

ระดับการคอมไพล์ ADVANCED_OPTIMIZATIONS จะทำการแปลงเหมือนกับ SIMPLE_OPTIMIZATIONS และยังเพิ่มการเปลี่ยนชื่อพร็อพเพอร์ตี้ ตัวแปร และฟังก์ชันทั่วโลก การกำจัดโค้ดที่ไม่ได้ใช้ และการลดระดับพร็อพเพอร์ตี้ การส่งผ่านใหม่เหล่านี้จะกำหนดข้อจำกัดเพิ่มเติมใน JavaScript ที่ป้อน โดยทั่วไป การใช้ ฟีเจอร์แบบไดนามิกของ JavaScript จะป้องกันการวิเคราะห์แบบคงที่ที่ถูกต้องใน โค้ดของคุณ

ผลกระทบของการเปลี่ยนชื่อตัวแปร ฟังก์ชัน และพร็อพเพอร์ตี้ส่วนกลาง

การเปลี่ยนชื่อทั่วโลกของ ADVANCED_OPTIMIZATIONS ทำให้แนวทางปฏิบัติ ต่อไปนี้เป็นอันตราย

  • การอ้างอิงภายนอกที่ไม่ได้ประกาศ:

    หากต้องการเปลี่ยนชื่อตัวแปร ฟังก์ชัน และพร็อพเพอร์ตี้ส่วนกลางอย่างถูกต้อง คอมไพเลอร์ต้องทราบการอ้างอิงทั้งหมดถึงตัวแปรส่วนกลางเหล่านั้น คุณต้องแจ้งคอมไพเลอร์เกี่ยวกับสัญลักษณ์ที่กำหนดไว้นอกโค้ดที่กำลังคอมไพล์ การคอมไพล์ขั้นสูงและ ไฟล์ภายนอกอธิบายวิธีกำหนดสัญลักษณ์ภายนอก

  • การใช้ชื่อภายในที่ไม่ได้ส่งออกในโค้ดภายนอก:

    โค้ดที่คอมไพล์แล้วต้องส่งออกสัญลักษณ์ใดๆ ที่โค้ดที่ยังไม่ได้คอมไพล์อ้างอิงถึง การคอมไพล์ขั้นสูงและ ไฟล์ภายนอกอธิบายวิธีส่งออกสัญลักษณ์

  • การใช้ชื่อสตริงเพื่ออ้างอิงพร็อพเพอร์ตี้ของออบเจ็กต์

    คอมไพเลอร์จะเปลี่ยนชื่อพร็อพเพอร์ตี้ในโหมดขั้นสูง แต่จะไม่เปลี่ยนชื่อสตริง

      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

    หากต้องการอ้างอิงพร็อพเพอร์ตี้ที่มีสตริงที่ยกมา ให้ใช้สตริงที่ยกมาเสมอ

      var x = { 'unrenamed_property': 1 };
      x['unrenamed_property'];  // This is OK.
      if ( 'unrenamed_property' in x ) {};   // This is OK
  • การอ้างอิงตัวแปรเป็นพร็อพเพอร์ตี้ของออบเจ็กต์ส่วนกลาง:

    คอมไพเลอร์จะเปลี่ยนชื่อพร็อพเพอร์ตี้และตัวแปรแยกกัน ตัวอย่างเช่น คอมไพเลอร์จะถือว่าการอ้างอิง 2 รายการต่อไปนี้ ถึง foo แตกต่างกัน แม้ว่าจะเทียบเท่ากันก็ตาม

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

    โค้ดนี้อาจคอมไพล์เป็น

      var a = {};
      window.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
    }
        

    คอมไพเลอร์ไม่เข้าใจว่ามีการเรียกใช้ initX() และ initY() ในลูป for จึงนำทั้ง 2 เมธอดนี้ออก

    โปรดทราบว่าหากคุณส่งฟังก์ชันเป็นพารามิเตอร์ คอมไพเลอร์จะค้นหาการเรียกพารามิเตอร์นั้นได้ ตัวอย่างเช่น คอมไพเลอร์จะไม่นำฟังก์ชัน getHello() ออกเมื่อคอมไพล์โค้ดต่อไปนี้ในโหมดขั้นสูง

    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.
        

ผลกระทบของการลดระดับพร็อพเพอร์ตี้ของออบเจ็กต์

ในโหมดขั้นสูง คอมไพเลอร์จะยุบคุณสมบัติของออบเจ็กต์เพื่อ เตรียมพร้อมสำหรับการย่อชื่อ ตัวอย่างเช่น คอมไพเลอร์จะเปลี่ยนโค้ดนี้

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

เป็นแบบนี้

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

การปรับพร็อพเพอร์ตี้นี้ให้แบนราบช่วยให้การเปลี่ยนชื่อในภายหลังมีประสิทธิภาพมากขึ้น คอมไพเลอร์สามารถแทนที่ foo$bar ด้วยอักขระเดียวได้ เช่น

แต่การทำให้พร็อพเพอร์ตี้แบนราบยังทำให้แนวทางปฏิบัติต่อไปนี้เป็นอันตรายด้วย

  • การใช้ this นอกตัวสร้างและ เมธอดต้นแบบ:

    การแปลงพร็อพเพอร์ตี้เป็นแบบแบนอาจเปลี่ยนความหมายของคีย์เวิร์ด this ภายในฟังก์ชัน เช่น

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

    กลายเป็น

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

    ก่อนการเปลี่ยนรูปแบบ this ภายใน foo.bar หมายถึง foo หลังจากการ แปลง this หมายถึง thisทั่วโลก ในกรณีเช่นนี้ คอมไพเลอร์จะสร้างคำเตือนต่อไปนี้

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

    หากต้องการป้องกันไม่ให้การแปลงพร็อพเพอร์ตี้ทำให้การอ้างอิงถึง this เสียหาย ให้ใช้ this ภายในตัวสร้างและเมธอดต้นแบบเท่านั้น ความหมายของ this นั้นชัดเจน เมื่อคุณเรียกใช้เครื่องมือสร้างด้วยคีย์เวิร์ด new หรือ ภายในฟังก์ชันที่เป็นพร็อพเพอร์ตี้ของ prototype

  • การใช้วิธีแบบคงที่โดยไม่ทราบว่ามีการเรียกใช้ในคลาสใด

    ตัวอย่างเช่น หากคุณมี:

    class A { static create() { return new A(); }};
    class B { static create() { return new B(); }};
    let cls = someCondition ? A : B;
    cls.create();
    คอมไพเลอร์จะยุบทั้ง 2 เมธอด create (หลังจากแปลงจาก ES6 เป็น ES5) ดังนั้น การเรียก cls.create() จะล้มเหลว คุณหลีกเลี่ยงปัญหานี้ได้โดยใช้คำอธิบายประกอบ @nocollapse
    class A {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
    class B {
      /** @nocollapse */
      static create() {
        return new A();
      }
    }
  • การใช้ super ในเมธอดแบบคงที่โดยไม่ทราบคลาสแม่

    โค้ดต่อไปนี้ปลอดภัย เนื่องจากคอมไพเลอร์ทราบว่า super.sayHi() หมายถึง Parent.sayHi()

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

    อย่างไรก็ตาม การปรับโครงสร้างพร็อพเพอร์ตี้จะทำให้โค้ดต่อไปนี้ใช้งานไม่ได้ แม้ว่า myMixin(Parent).sayHi จะเท่ากับ Parent.sayHi ที่ยังไม่ได้คอมไพล์ก็ตาม

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

    หลีกเลี่ยงการหยุดทำงานนี้ด้วยคำอธิบายประกอบ /** @nocollapse */

  • การใช้ Object.defineProperties หรือตัวรับ/ตัวตั้งค่า ES6:

    คอมไพเลอร์ไม่เข้าใจโครงสร้างเหล่านี้ดีนัก ตัวรับและตัวตั้งค่า ES6 จะได้รับการแปลง เป็น Object.defineProperties(...) ผ่านการแปลง ปัจจุบันคอมไพเลอร์ ไม่สามารถวิเคราะห์โครงสร้างนี้แบบคงที่ และถือว่าการเข้าถึงและการตั้งค่าพร็อพเพอร์ตี้ไม่มีผลข้างเคียง ซึ่งอาจส่งผลกระทบที่เป็นอันตราย เช่น

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

    คอมไพล์เป็น

    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 ไม่มีผลข้างเคียง จึงนำออก