فهم القيود التي يفرضها Closure Compiler

يتوقّع Closure Compiler أن يتوافق إدخال JavaScript مع بعض القيود. كلما زاد مستوى التحسين الذي تطلبه من برنامج الترجمة البرمجية، زادت القيود التي يفرضها برنامج الترجمة البرمجية على JavaScript المُدخَل.

يوضّح هذا المستند القيود الرئيسية لكل مستوى من مستويات التحسين. يمكنك أيضًا الاطّلاع على صفحة الويكي هذه لمعرفة الافتراضات الإضافية التي يضعها المترجم.

القيود المفروضة على جميع مستويات التحسين

يفرض Compiler القيدَين التاليَين على جميع رموز 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() قد تغريك باستخدام أسماء الوسيطات في الرمز البرمجي، إلا أنّ عملية تجميع الوضع البسيط تؤدي إلى إيقاف هذا النوع من المراجع.

قيود ADVANCED_OPTIMIZATIONS

ينفّذ مستوى التجميع ADVANCED_OPTIMIZATIONS عمليات التحويل نفسها التي ينفّذها SIMPLE_OPTIMIZATIONS، كما يضيف عمليات إعادة تسمية شاملة للسمات والمتغيرات والدوال، وإزالة الرموز غير المستخدَمة، وتسوية السمات. تفرض عمليات التحقّق الجديدة هذه قيودًا إضافية على JavaScript المُدخَل. بشكل عام، سيؤدي استخدام الميزات الديناميكية في JavaScript إلى منع التحليل الثابت الصحيح للرمز البرمجي.

تداعيات إعادة تسمية المتغيرات والدوال والخصائص العامة:

يؤدي تغيير الاسم العام لـ ADVANCED_OPTIMIZATIONS إلى خطورة الممارسات التالية:

  • المراجع الخارجية غير المعلَنة:

    لكي تتم إعادة تسمية المتغيرات العامة والدوال والسمات بشكل صحيح، يجب أن يعرف المترجم جميع المراجع إلى تلك العناصر العامة. يجب إخبار المترجم عن الرموز التي تم تحديدها خارج الرمز الذي تتم ترجمته. توضّح مقالة الترجمة البرمجية المتقدّمة و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.
        

تأثيرات تسوية سمة العنصر

في "الوضع المتقدّم"، يقلّل "المترجم" من حجم سمات الكائن استعدادًا لتقصير الأسماء. على سبيل المثال، يحوّل Compiler ما يلي:

   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();
    سيصغّر المترجم كلا الطريقتين 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 ليس له أي آثار جانبية، لذا تمت إزالته.