فيه سؤال JavaScript بيطلع في 70% من الـ interviews وبيكسر سكربتات الإنتاج لحد دلوقتي: ليه for loop فيه setTimeout بيطبع رقم 6 خمس مرات بدل 1، 2، 3، 4، 5؟ الإجابة اسمها Closures، وهي من أهم 3 مفاهيم في اللغة كلها. لو فهمتها صح، هتمنع bugs بتكلّف ساعات debugging.
Closures في JavaScript: المفهوم اللي بيكسر سكربتاتك بدون ما تحس
المشكلة بالظبط
افتح console المتصفح والصق الكود ده:
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
المتوقّع: 1، 2، 3، 4، 5. اللي بيحصل فعلاً: 6، 6، 6، 6، 6. الكود مش معطوب، لكن JavaScript بتفسّر الـ scope بطريقة مختلفة عن اللي توقّعتها.
قبل ما نشرح المفهوم — مثال من العالم الحقيقي
تخيّل إنك في مطعم فيه شباك واحد للطلبات. الكاشير عنده ورقة واحدة بس بيكتب عليها رقم آخر طلب بقلم رصاص (وبيمسح كل مرة). 5 عملاء بيدفعوا واحد ورا التاني، فالكاشير بيكتب 1 ثم يمسح ويكتب 2 ثم 3 ثم 4 ثم 5. الورقة في النهاية فيها رقم 5 بس.
بعد ما خلصوا الـ 5 طلبات، 5 موظفين تحضير دخلوا في نفس اللحظة وقروا الورقة. كلهم شافوا 5 — لأن الورقة واحدة، مش 5 نسخ.
الـ var في JavaScript هي الورقة دي بالظبط. كل iteration في الـ loop بتعدّل نفس المتغير في الذاكرة، مش بتنشئ متغير جديد. لما setTimeout بتشتغل بعد 100 مللي ثانية، الـ loop خلصت من زمان والـ i قيمته بقت 6 (لأن الشرط i <= 5 فشل عند 6 وبعدين خرج).
التعريف العلمي للـ Closure
Closure هي function بتفتكر الـ scope اللي اتعرّفت فيه، حتى لو الـ scope ده خلص. المواصفة في ECMAScript 2024 (section 8.4) بتقول: "كل function بتحتفظ بـ reference للـ Lexical Environment الخاصة بمكان تعريفها".
الكلمة المفتاحية هنا: reference، مش copy. لو المتغير اتغيّر بعد كده في الـ scope الأصلي، الـ closure بتشوف القيمة الجديدة لمّا تشتغل.
ده اللي بيحصل في الكود فوق: الـ 5 functions اللي جوّا setTimeout كلهم closures بيشيروا لنفس المتغير i. لما الوقت يجي، كلهم بيقروا قيمته الحالية (6).