הסבר על ההגבלות שמוטלות על ידי Closure Compiler

הקומפיילר של Closure מצפה שקלט ה-JavaScript שלו יתאים לכמה הגבלות. ככל שרמת האופטימיזציה שאתם מבקשים מהקומפיילר לבצע גבוהה יותר, כך הקומפיילר מטיל יותר הגבלות על קלט ה-JavaScript.

במסמך הזה מפורטות ההגבלות העיקריות לכל רמת אופטימיזציה. אפשר גם לעיין בדף הוויקי הזה כדי לקבל מידע על הנחות נוספות שהקומפיילר מבצע.

הגבלות לכל רמות האופטימיזציה

הקומפיילר מטיל את שתי המגבלות הבאות על כל קוד 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

ברמת האופטימיזציה Simple, פרמטרים של פונקציות, משתנים מקומיים ופונקציות שמוגדרות באופן מקומי מקבלים שמות חדשים כדי להקטין את גודל הקוד. עם זאת, חלק ממבני JavaScript עלולים לשבש את תהליך השינוי הזה.

מומלץ להימנע מהמבנים ומהשיטות הבאים כשמשתמשים ב-SIMPLE_OPTIMIZATIONS:

  • with:

    כשמשתמשים ב-with, הקומפיילר לא יכול להבחין בין משתנה מקומי לבין מאפיין אובייקט עם אותו שם, ולכן הוא משנה את השם של כל המופעים של השם.

    בנוסף, השימוש בהצהרה with מקשה על אנשים לקרוא את הקוד. ההצהרה with משנה את הכללים הרגילים לזיהוי שמות, ויכולה להקשות על זיהוי ההפניה של שם מסוים, גם על המתכנת שכתב את הקוד.

  • eval():

    הקומפיילר לא מנתח את ארגומנט המחרוזת של eval(), ולכן הוא לא ישנה את השם של אף סמל בארגומנט הזה.

  • ייצוגים של מחרוזות של שמות פונקציות או פרמטרים:

    הקומפיילר משנה את השמות של פונקציות ופרמטרים של פונקציות, אבל הוא לא משנה מחרוזות בקוד שמתייחסות לפונקציות או לפרמטרים לפי שם. לכן, לא מומלץ לייצג שמות של פונקציות או פרמטרים כמחרוזות בקוד. לדוגמה, הפונקציה argumentNames() בספריית Prototype משתמשת ב-Function.toString() כדי לאחזר את שמות הפרמטרים של פונקציה. אבל למרות שargumentNames() יכולה לגרום לכם להשתמש בשמות של ארגומנטים בקוד, קומפילציה במצב פשוט שוברת הפניה כזו.

הגבלות על ADVANCED_OPTIMIZATIONS

ברמת הקומפילציה ADVANCED_OPTIMIZATIONS מתבצעים אותם שינויים כמו ברמה SIMPLE_OPTIMIZATIONS, ונוספים גם שינויים גלובליים בשמות של מאפיינים, משתנים ופונקציות, הסרת קוד מת, ושיטוח של מאפיינים. הכרטיסים החדשים האלה מטילים הגבלות נוספות על קלט ה-JavaScript. באופן כללי, שימוש בתכונות דינמיות של JavaScript ימנע ניתוח סטטי נכון של הקוד.

ההשלכות של שינוי השם של משתנה, פונקציה ומאפיין גלובליים:

השינוי הגלובלי של השם ADVANCED_OPTIMIZATIONS הופך את הפעולות הבאות למסוכנות:

  • הפניות חיצוניות שלא הוכרזו:

    כדי לשנות את השם של משתנים גלובליים, פונקציות ומאפיינים בצורה נכונה, הקומפיילר צריך לדעת על כל ההפניות למשתנים הגלובליים האלה. צריך להודיע לקומפיילר על סמלים שמוגדרים מחוץ לקוד שעובר קומפילציה. במאמר Advanced Compilation and Externs (קומפילציה מתקדמת ו-Externs) מוסבר איך להצהיר על סמלים חיצוניים.

  • שימוש בשמות פנימיים שלא יוצאו בקוד חיצוני:

    קוד שעבר קומפילציה חייב לייצא את כל הסמלים שהקוד שלא עבר קומפילציה מתייחס אליהם. במאמר Advanced Compilation and Externs (קומפילציה מתקדמת וקובצי Externs) מוסבר איך לייצא סמלים.

  • שימוש בשמות מחרוזות כדי להפנות למאפייני אובייקט:

    הקומפיילר משנה את השם של מאפיינים במצב מתקדם, אבל הוא אף פעם לא משנה את השם של מחרוזות.

      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
  • התייחסות למשתנים כמאפיינים של האובייקט הגלובלי:

    הקומפיילר משנה את השמות של המאפיינים והמשתנים באופן עצמאי. לדוגמה, הקומפיילר מתייחס לשני ההפניות הבאות אל 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, ולכן הוא מסיר את שתי השיטות האלה.

    שימו לב שאם מעבירים פונקציה כפרמטר, הקומפיילר יכול למצוא קריאות לפרמטר הזה. לדוגמה, הקומפיילר לא מסיר את הפונקציה 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 מחוץ ל-constructors ול-prototype methods:

    השטחת מאפיינים יכולה לשנות את המשמעות של מילת המפתח 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 within foo.bar מתייחס ל-foo. אחרי ההמרה, this מתייחס ל-this הגלובלי. במקרים כמו זה, המהדר מפיק את האזהרה הבאה:

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

    כדי למנוע מצב שבו השטחת המאפיינים תשבור את ההפניות שלכם אל this, צריך להשתמש ב-this רק בתוך בנאים ובשיטות אב-טיפוס. המשמעות של this היא חד-משמעית כשקוראים לפונקציית constructor עם מילת המפתח new, או בתוך פונקציה שהיא מאפיין של prototype.

  • שימוש בשיטות סטטיות בלי לדעת באיזה מחלקה הן נקראות:

    לדוגמה, אם יש לך:

    class A { static create() { return new A(); }};
    class B { static create() { return new B(); }};
    let cls = someCondition ? A : B;
    cls.create();
    הקומפיילר יכווץ את שתי השיטות 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 או ב-getter/setters של ES6:

    הקומפיילר לא מבין את המבנים האלה. פונקציות getter ו-setter של 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 אין תופעות לוואי, ולכן היא הוסרה.