استخدام دع بيانات التعريفات للمتغيرات وميزات عمليات الإغلاق الناتجة في JavaScript

كنت مصدر إلهام لكتابة هذه المذكرة من خلال قراءة المقال على Habré "فار ، دعونا أم كون؟ مشاكل نطاق المتغيرات و ES6 " والتعليقات عليها ، وكذلك الجزء المقابل من كتاب Zakas N." فهم ECMAScript 6 " . بناءً على ما قرأته ، توصلت إلى استنتاج مفاده أن ليس كل شيء بسيطًا جدًا في تقييم استخدام var أو السماح . يميل المؤلفون والمعلقون إلى الاعتقاد بأنه في غياب الحاجة إلى دعم الإصدارات القديمة من المتصفحات ، فمن المنطقي التخلي تمامًا عن استخدام var ، وكذلك استخدام بعض الإنشاءات المبسطة ، بدلاً من القديمة ، افتراضيًا.

لقد قيل بالفعل عن نطاق هذه الإعلانات ، بما في ذلك المواد المذكورة أعلاه ، لذلك أود التركيز فقط على بعض النقاط غير الواضحة.

بادئ ذي بدء ، أود أن أعتبر تعبيرات الدالات التي تُسمى على الفور (تعبير الدالة المستدعى فورًا ، IIFE) في الحلقات.
let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); } func1.forEach(function(func) { func(); }); /*    0 newECMA6add.js:4:59 1 newECMA6add.js:4:59 2 newECMA6add.js:4:59 */ 

أو يمكنك الاستغناء عنها باستخدام let :

 let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); } func1.forEach(function(func) { func(); }); /*     0 newECMA6add.js:4:37 1 newECMA6add.js:4:37 2 newECMA6add.js:4:37 */ 

يدعي Zakas N. أن كلا المثالين المتشابهين ، مع إعطاء نفس النتيجة ، يعملان أيضًا بنفس الطريقة:
"هذه الحلقة تعمل تمامًا مثل الحلقة التي استخدمت var و IIFE ولكن يمكن القول إنها أنظف"
الذي ، ومع ذلك ، هو نفسه ، أبعد قليلا ، يدحض بشكل غير مباشر.

الحقيقة هي أن كل تكرار للحلقة عند الاستخدام يخلق متغير محلي منفصل i ، في حين أن الربط في الدالات المرسلة إلى الصفيف يذهب أيضًا إلى فصل المتغيرات عن كل تكرار.

في هذه الحالة بالذات ، لا تكون النتيجة مختلفة حقًا ، ولكن ماذا لو قمنا بتعقيد الكود قليلاً؟
 let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); /*    0 newECMA6add.js:4:59 2 newECMA6add.js:4:59 */ 

هنا ، بإضافة ++ i ، تبين أن نتائجنا يمكن التنبؤ بها تمامًا ، حيث قمنا بتسمية الدالة ذات القيم i التي كانت ذات صلة في وقت المكالمة حتى عندما مرت الحلقة نفسها ، وبالتالي لم تؤثر العملية اللاحقة ++ على القيمة التي تم تمريرها إلى الوظيفة في المصفوفة ، حيث إنها بالفعل تم إغلاقها في الوظيفة (i) بقيمة محددة من i .

قارن الآن مع إصدار Let بدون IIFE
 let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); }); /*    1 newECMA6add.js:4:37 3 newECMA6add.js:4:37 */ 

النتيجة ، على ما يبدو ، قد تغيرت ، وطبيعة هذا التغيير هي أننا لم ندعو الوظيفة ذات القيمة على الفور ، لكن الوظيفة أخذت القيم المتاحة في عمليات الإغلاق بتكرار محدد من الدورة.

لفهم جوهر ما يحدث بشكل أفضل ، فكر في الأمثلة مع صفيفين. بالنسبة للمبتدئين ، لنأخذ var ، دون IIFE :
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    5 newECMA6add.js:6:37 6 newECMA6add.js:6:37 7 newECMA6add.js:5:37 8 newECMA6add.js:5:37 */ 

كل شيء هنا واضح حتى الآن - لا يوجد أي إغلاق (على الرغم من أننا يمكن أن نقول أنه كذلك ، ولكن على النطاق العالمي ، رغم أن هذا ليس صحيحًا تمامًا ، لأن الوصول إلى i موجود بشكل أساسي في كل مكان) ، على سبيل المثال ، ولكن مع منطقة محلية على ما يبدو ، سيكون لدي متغير إدخال مماثل:
 let func1 = [], func2 = []; function test() { for (var i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } } test(); func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*     5 newECMA6add.js:7:41 6 newECMA6add.js:7:41 7 newECMA6add.js:6:41 8 newECMA6add.js:6:41 */ 

في كلا المثالين ، يحدث ما يلي:

1. في بداية آخر تكرار للدورة i == 2 ، ثم يتم زيادتها بنسبة 1 (++ i) ، وفي النهاية تتم إضافة 1 إضافي من i ++ ، ونتيجة لذلك ، في نهاية الدورة بأكملها i == 4 .

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

أضف IIFE .
الخيار الأول:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(++i); } }(i)); func1.push(function(i) { return function() { console.log(++i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    1 newECMA6add.js:6:56 3 newECMA6add.js:6:56 1 newECMA6add.js:5:56 3 newECMA6add.js:5:56 */ 
الخيار الثاني:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(i); } }(++i)); func1.push(function(i) { return function() { console.log(i); } }(++i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    2 newECMA6add.js:6:56 1 newECMA6add.js:5:56 */ 

عند إضافة IIFE في الحالة الأولى ، أطلقنا ببساطة القيم الثابتة للدالة i في الوظيفة (i) ( 0 و 2 ، أثناء التمريرين الأول والثاني من الدورة ، على التوالي) ، وزادناها بواحد ، كل وظيفة منفصلة عن الأخرى ، حيث يوجد الإغلاق للمتغير المشترك لا توجد حلقة ، نظرًا لحقيقة أن القيمة التي تم إرسالها فورًا أثناء مرور الحلقة. في الحالة الثانية ، لا يوجد أي إغلاق لمتغير الحلقة ، ولكن هناك تم نقل القيمة مع زيادة متزامنة ، لذلك في نهاية التمريرة الأولى i == 4 ، ولم تذهب الحلقة إلى أبعد من ذلك. لكنني أسترعي الانتباه إلى حقيقة أن عمليات إغلاق المتغيرات من الوظائف الخارجية في الوظائف الداخلية ، لكل وظيفة على حدة ، لا تزال موجودة في الخيارين الأول والثاني. على سبيل المثال:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(++i); } }(i)); func1.push(function(i) { return function() { console.log(++i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    1 newECMA6add.js:6:56 3 newECMA6add.js:6:56 1 newECMA6add.js:5:56 3 newECMA6add.js:5:56 2 newECMA6add.js:6:56 4 newECMA6add.js:6:56 2 newECMA6add.js:5:56 4 newECMA6add.js:5:56 */ 
ملاحظة: حتى لو قمت بتأطير الدورة بوظيفة ، فإن عمليات الإغلاق الشائعة لن تفعل ذلك بشكل طبيعي.

الآن فكر في عبارة let ، بدون IIFE ، على التوالي.
 let func1 = [], func2 = []; for (let i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    2 newECMA6add.js:6:41 4 newECMA6add.js:6:41 3 newECMA6add.js:5:41 5 newECMA6add.js:5:41 */ 

وهنا ، قمنا مرة أخرى بتشكيل دائرة كهربائية قصيرة لمتغير الحلقة ، وليس واحدًا ، ولكن اثنين ، وليس منفصلين ، ولكنهما شائعان ، وهو أمر منطقي ، نظرًا لمبدأ السماح بالدخول في الدورات.

نتيجة لذلك ، لدينا ذلك في الإغلاق الأول ، وقبل استدعاء الوظائف في المصفوفات ، تكون القيمة هي i == 1 ، والثانية i == 3 . هذه هي القيم التي تلقيتها المتغير قبل i ++ وتكرار الحلقة ، ولكن بعد كل الإرشادات في كتلة الحلقة ، وأنها مغلقة لكل تكرار محدد.

ثم يتم استدعاء الوظائف الموجودة في صفيف func1 وهي تزيد المتغيرات المقابلة في كل من الإغلاقات ونتيجة لذلك في i == 2 الأولى ، وفي i == 4 الثانية.

الدعوة اللاحقة إلى func2 تزداد أكثر وتحصل على i == 3 و 5 على التوالي.

لقد وضعت عمدا func2 و func1 داخل الكتلة بطريقة جعلت الاستقلال عن موقعهما أكثر وضوحا ، وللتأكيد على انتباه القارئ إلى حقيقة إغلاق المتغيرات الحلقية.

في الختام ، سأقدم مثالًا تافهًا يهدف إلى تعزيز فهم عمليات الإغلاق ونطاق السماح :
 let func1 = []; { let i = 0; func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); }); console.log(i); /* 1 newECMA6add.js:5:34 ReferenceError: i is not definednewECMA6add.js:10:1 */ 

ماذا لدينا في المجموع


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

2. نظرًا لحقيقة أنه عند استخدام تصريح let للتكرار ، يتم إنشاء متغير محلي منفصل في كل تكرار ، ينشأ سؤال حول التخلص من البيانات غير الضرورية من قبل جامع البيانات المهملة. في هذه المرحلة ، أعترف أنني أردت تركيز الاهتمام مبدئيًا ، معتقدًا أن إنشاء عدد كبير من المتغيرات في الحلقات الكبيرة ، على التوالي ، سيؤدي إلى إبطاء برنامج التحويل البرمجي ، ومع ذلك ، عند فرز صفيف اختبار باستخدام إعلانات السماح المتغيرة فقط ، فقد أظهر زيادة في وقت التنفيذ تقريبًا مرتين لمجموعة من 100000 خلية:
الخيار مع فار:
 const start = Date.now(); var arr = [], func1 = [], func2 = []; for (var i = 0; i < 100000; i++) { arr.push(Math.random()); } for (var i = 0; i < 99999; i++) { var min, minind = i; for (var j = i + 1; j < 100000; j++) { if (arr[minind] > arr[j]) minind = j; } min = arr[minind]; arr[minind] = arr[i]; arr[i] = min; func1.push(function(i) { return function() { return i; } }(arr[i])); } func1.push(function(i) { return function() { return i; } }(arr[99999])); for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); } const end = Date.now(); console.log((end - start)/1000); // 9.847 


والخيار مع اسمحوا:
 const start = Date.now(); let arr = [], func1 = [], func2 = []; for (let i = 0; i < 100000; i++) { arr.push(Math.random()); } for (let i = 0; i < 99999; i++) { let min, minind = i; for (let j = i + 1; j < 100000; j++) { if (arr[minind] > arr[j]) minind = j; } min = arr[minind]; arr[minind] = arr[i]; arr[i] = min; func1.push(function() { return arr[i]; }); } func1.push(function() { return arr[99999]; }); for (let i = 0; i < 100000; i++) { func2.push(func1[i]()); } const end = Date.now(); console.log((end - start)/1000); // 5.3 


في الوقت نفسه ، كان وقت التنفيذ مستقلاً عملياً عن وجود / عدم وجود تعليمات:

مع IIFE
 func1.push(function(i) { return function() { return i; } }(arr[i])); 

أو
بدون IIFE
 func1.push(function() { return arr[i]; }); 

و
استدعاء وظيفة
 for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); } 


ملاحظة: أفهم أن المعلومات المتعلقة بالسرعة ليست جديدة ، ولكن للتأكد من اكتمالها ، أعتقد أن هذين المثالين يستحقان العطاء.

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

Source: https://habr.com/ru/post/ar462971/


All Articles