كنت مصدر إلهام لكتابة هذه المذكرة من خلال قراءة المقال على 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(); });
أو يمكنك الاستغناء عنها باستخدام
let :
let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); } func1.forEach(function(func) { func(); });
يدعي
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(); });
هنا ، بإضافة
++ 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(); });
النتيجة ، على ما يبدو ، قد تغيرت ، وطبيعة هذا التغيير هي أننا لم ندعو الوظيفة ذات القيمة على الفور ، لكن الوظيفة أخذت القيم المتاحة في عمليات الإغلاق بتكرار محدد من الدورة.
لفهم جوهر ما يحدث بشكل أفضل ، فكر في الأمثلة مع صفيفين. بالنسبة للمبتدئين ، لنأخذ 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(); });
كل شيء هنا واضح حتى الآن - لا يوجد أي إغلاق (على الرغم من أننا يمكن أن نقول أنه كذلك ، ولكن على النطاق العالمي ، رغم أن هذا ليس صحيحًا تمامًا ، لأن الوصول إلى
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(); });
في كلا المثالين ، يحدث ما يلي:
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(); });
الخيار الثاني:
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(); });
عند إضافة
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(); });
ملاحظة: حتى لو قمت بتأطير الدورة بوظيفة ، فإن عمليات الإغلاق الشائعة لن تفعل ذلك بشكل طبيعي.الآن فكر في عبارة
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(); });
وهنا ، قمنا مرة أخرى بتشكيل دائرة كهربائية قصيرة لمتغير الحلقة ، وليس واحدًا ، ولكن اثنين ، وليس منفصلين ، ولكنهما شائعان ، وهو أمر منطقي ، نظرًا لمبدأ
السماح بالدخول في الدورات.
نتيجة لذلك ، لدينا ذلك في الإغلاق الأول ، وقبل استدعاء الوظائف في المصفوفات ، تكون القيمة هي
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. إن استدعاء تعبيرات الوظائف التي يتم استدعاؤها فورًا لا يعادل استخدام متغيرات
السماح المتكرر في الوظائف في الحلقات ، وفي بعض الحالات ، يؤدي إلى نتائج مختلفة.
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);
والخيار مع اسمحوا: 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);
في الوقت نفسه ، كان وقت التنفيذ مستقلاً عملياً عن وجود / عدم وجود تعليمات:
مع 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 ، في التطبيقات التي لا تتطلب توافقًا مع الإصدارات السابقة مع المعايير السابقة ، هو أكثر من مبرر ، خاصةً في الحالات مع الحلقات. ولكن ، في الوقت نفسه ، يجدر تذكر سمات السلوك في المواقف التي يتم فيها الإغلاق ، وإذا لزم الأمر ، استمر في استخدام تعبيرات الوظائف التي تسمى فورًا.