فهم القيود المفروضة من قِبل مجمّع الإغلاق

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

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

قيود على جميع مستويات التحسين

وتضع العارضة القيدين التاليين على جميع جافا سكريبت التي تعالجها، وذلك لجميع مستويات التحسين:

  • لا يتعرّف المُجمِّع إلا على ECMAScript.

    ECMAScript 5 هو إصدار JavaScript المستخدم في كل مكان تقريبًا. ولكن المحول البرمجي يعتمد أيضًا العديد من الميزات في ECMAScript 6. لا تتيح خدمة التجميع سوى استخدام ميزات اللغة الرسمية فقط.

    ستعمل الميزات الخاصة بالمتصفح التي تتوافق مع مواصفات لغة ECMAScript المناسبة على نحو جيد مع المحول البرمجي. على سبيل المثال، يتم إنشاء كائنات ActiveX ببنية جافا سكريبت قانونية، لذلك تعمل الشفرة التي تنشئ كائنات ActiveX مع المحول البرمجي.

    تعمل أدوات التجميع في البرمجيات على دعم إصدارات اللغات الجديدة وميزاتها. يمكن للمشاريع تحديد إصدار لغة ECMAScript الذي تنوي استخدامه باستخدام العلامة --language_in.

  • لا يحتفظ منشئ المحتوى بالتعليقات.

    تعمل جميع مستويات تحسين Compiler على إزالة التعليقات، لذلك فإن الشفرة التي تعتمد على التعليقات المنسقة بشكل خاص لا تعمل مع Compiler.

    على سبيل المثال، نظرًا لأن أداة التجميع لا تحتفظ بالتعليقات، لا يمكنك استخدام "التعليقات المشروطة" لجافا سكريبت مباشرة. يمكنك في هذه الحالة الالتفاف حول هذا التقييد من خلال التفاف التعليقات الشرطية في تعبيرات eval(). يمكن للمعالج معالجة الرمز التالي بدون حدوث خطأ:

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

    ملاحظة: يمكنك تضمين تراخيص البرامج مفتوحة المصدر وغير ذلك من النصوص المهمة في الجزء العلوي من ناتج برنامج التحويل البرمجي باستخدام التعليق التوضيحي @preserve

قيود SIMPLE_OPTIMIZATIONS

يعمل مستوى التحسين البسيط على إعادة تسمية معلمات الوظائف، والمتغيرات المحلية، والدوال المحددة محليًا لتقليل حجم الشفرة. ومع ذلك، يمكن أن تؤدي بعض عمليات إنشاء جافا سكريبت إلى تعطيل عملية إعادة التسمية هذه.

تجنَّب استخدام التراكيب والممارسات التالية عند استخدام SIMPLE_OPTIMIZATIONS:

  • with:

    عند استخدام السمة with، لا يمكن للمُجمِّع التمييز بين متغيّر محلي وخاصية عنصر يحمل الاسم نفسه، وبالتالي سيُعيد تسمية كل مثيلات الاسم.

    بالإضافة إلى ذلك، تجعل عبارة with من الصعب على المستخدمين قراءة رمزك. تغيّر عبارة with القواعد العادية لدقة الاسم، ويمكن أن تزيد صعوبة الأمر حتى بالنسبة إلى المبرمج الذي كتب الرمز لتحديد ما يشير إليه الاسم.

  • eval():

    لا تحلل أداة التجميع السلسلة الوسيطة لـ eval()، وبالتالي لن تعيد تسمية أي رموز داخل هذه الوسيطة.

  • تمثيلات السلاسل لأسماء الدوال أو المعلّمات:

    يعيد برنامج التجميع تسمية الدوال ومَعلمات الدوال، ولكنه لا يغيّر أي سلاسل في الرمز تشير إلى الدوال أو المعلّمات حسب الاسم. وبالتالي، عليك تجنُّب تمثيل أسماء الدوال أو المعلّمات كسلاسل في الرمز. على سبيل المثال، تستخدم دالة مكتبة النماذج الأولية argumentNames() السمة Function.toString() لاسترداد أسماء معلّمات الدالة. قد تغريك argumentNames() باستخدام أسماء الوسيطات في الرمز الخاص بك، إلا أنّ التجميع في الوضع البسيط يخالف هذا النوع من المراجع.

قيود ADVANCED_OPTIMIZATIONS

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

الآثار المترتبة على إعادة تسمية المتغير العام والدالة والسمة:

إنّ إعادة تسمية ADVANCED_OPTIMIZATIONS عالميًا تجعل الممارسات التالية خطيرة:

  • مراجع خارجية غير معرَّفة:

    لإعادة تسمية المتغيرات والوظائف والخصائص العامة بشكل صحيح، يجب أن يكون المؤلف على دراية بجميع الإشارات إلى هذه العناصر العمومية. يجب أن تخبر العارض عن الرموز التي يتم تعريفها خارج الشفرة التي يتم تجميعها. تصف التجميعات المتقدمة والخارجية كيفية الإعلان عن الرموز الخارجية.

  • استخدام أسماء داخلية لم يتم تصديرها في رمز خارجي:

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

    يمكن أن يؤدي تسطيح الموقع إلى تغيير معنى الكلمة الرئيسية 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 أو getter/setter في 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 ليس له أي آثار جانبية ولذلك تمت إزالته.