مستوى المقال: مبتدئ. لو لسه ماسك JavaScript من شوية وسمعت كلمة Closure وحسّيت إنها سحر، المقال ده هيخليها أوضح حاجة عندك في اللغة.
لو دالة في JavaScript رجّعتلك رقم بيزيد كل مرة تناديها — رغم إن الدالة الأصلية خلصت وانتهت — انت بالظبط قدام الـ Closure. ده مش bug ولا حاجة غريبة. ده أهم مفهوم في اللغة، ولو فهمته هتكتب كود أنضف وتبطّل تخاف من الأخطاء اللي ملهاش معنى.
Closures في JavaScript: الدالة اللي بتفتكر
المشكلة باختصار
المتغيّر اللي بتعرّفه جوه دالة المفروض يموت لما الدالة تخلّص شغلها. ده السلوك الطبيعي. لكن في JavaScript، لو دالة جوّانية رجعت وهي لسه "ماسكة" المتغيّر ده، المتغيّر بيفضل عايش. الكومبيوتر بيحتفظ بيه في الذاكرة عشان الدالة الجوّانية لسه محتاجاه. ده اللي اسمه Closure.
ليه ده مهم؟ لإنه أساس حاجات بتستخدمها كل يوم: العدّادات، الـ event handlers، الـ private variables، وأغلب أدوات الـ state في React. ومن غير ما تفهمه، هتقابل أخطاء مش هتعرف تفسّرها.
المثال البسيط الأول: خزانة المدرسة
تخيّل إن في مدرسة، كل طالب بياخد خزانة (locker) ليها مفتاح واحد بس. الطالب يمشي من المدرسة، يروح بيته، يرجع تاني السنة الجاية — الخزانة لسه فيها كتبه، والمفتاح لسه شغّال. المدرسة (النطاق الخارجي) مقفلت أبوابها، بس خزانتك انت لسه محفوظة لإنك ماسك مفتاحها.
الدالة الجوّانية هي الطالب. المتغيّر هو الخزانة. والمفتاح هو الـ Closure: الرابط اللي بيخلّي الطالب يوصل لخزانته حتى بعد ما الدوام خلص. باقي الطلاب مش هيقدروا يفتحوا خزانتك — كل واحد له خزانته لوحده. خليك فاكر النقطة دي، هي مهمة في آخر المقال.
التعريف العلمي الدقيق
بعد ما المثال وضّح الفكرة، خلّينا ندقّق. الـ Closure حسب توثيق Mozilla MDN هو: "تركيبة من دالة مع البيئة المعجمية (Lexical Environment) اللي اتعرّفت جواها الدالة دي". يعني الدالة بتحمل معاها مرجع لكل المتغيّرات اللي كانت متاحة لحظة تعريفها، مش لحظة استدعائها.
"المعجمي" (Lexical) هنا معناها: النطاق بيتحدّد من مكان كتابة الكود في الملف، مش من مكان مناداته. ده فرق جوهري. JavaScript بتقرر مين بيشوف مين بناءً على شكل الكود وانت بتكتبه، قبل ما يشتغل أصلاً. المواصفة الرسمية ECMAScript بتسمّي ده Lexical Scoping، وهو نفس المبدأ في لغات زي Python و Swift.
الكود الشغّال: عدّاد بيفتكر
ده أبسط Closure ممكن تكتبه. جرّبه على Node 22 أو في console المتصفح مباشرة:
function createCounter() {
let count = 0; // ده "الخزانة"
return function () {
count = count + 1; // الدالة الجوّانية ماسكة المفتاح
return count;
};
}
const next = createCounter();
console.log(next()); // 1
console.log(next()); // 2
console.log(next()); // 3
لاحظ حاجة غريبة: createCounter() اتنفّذت مرة واحدة بس وخلصت. المفروض count يموت معاها. بس كل مرة تنادي next() الرقم بيزيد. ده معناه إن count لسه عايش في الذاكرة، محفوظ جوه الـ Closure. وأهم نقطة: لو عملت const next2 = createCounter() هتلاقي next2 ليه عدّاد منفصل تماماً يبدأ من 1 — زي خزانة جديدة بمفتاح جديد.
الخطأ الأشهر للمبتدئين: الـ for loop
الكود ده بيوقّع 90% من اللي بيتعلموا. خمّن المخرجات قبل ما تكمّل:
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function () {
return i;
});
}
console.log(funcs[0](), funcs[1](), funcs[2]());
// المتوقع: 0 1 2 — الحقيقي: 3 3 3
ليه طلعت 3 3 3؟ لإن var بيعمل متغيّر واحد مشترك على مستوى الدالة كلها. التلات دوال ماسكين نفس الخزانة، ولما الـ loop خلص قيمة i بقت 3. فكلهم بيقروا 3.
الحل في سطر واحد: غيّر var لـ let. الـ let بيعمل متغيّر جديد لكل لفّة في الـ loop، فكل دالة بتاخد خزانتها الخاصة:
for (let i = 0; i < 3; i++) {
funcs.push(() => i);
}
console.log(funcs[0](), funcs[1](), funcs[2]()); // 0 1 2
الفرق ده مش تفصيلة. ده سبب رئيسي إن let اتضاف أصلاً في ES2015 (ECMAScript 6). الافتراض هنا إنك بتكتب كود حديث؛ لو لسه شايف var في كود قديم، ده أول مكان تشك فيه لما تلاقي قيم غريبة.
سيناريو واقعي ورقم
لو عندك تطبيق فيه 1000 زرار، وكل زرار محتاج يفتكر بياناته الخاصة (id مثلاً)، الـ Closure بيوفّر عليك إنك تخزّن الـ id في الـ DOM وترجع تقراه كل مرة. القراءة من DOM بتاخد حوالي 0.1ms لكل عملية؛ القراءة من Closure في الذاكرة بتاخد أقل من 0.001ms — أسرع حوالي 100 مرة في الحالة دي. المكسب مش ضخم لزرار واحد، بس لو بتعالج آلاف الـ handlers في الثانية بيفرق فعلاً.
الـ trade-offs اللي لازم تعرفها
- الذاكرة: المتغيّر اللي ماسكه الـ Closure مش بيتمسح بالـ Garbage Collector. بتكسب إنه بيفضل متاح، بتخسر إنه بياخد مكان في الرام طول ما الدالة عايشة.
- الوضوح: الـ Closures بتخفي الـ state. بتكسب تغليف (encapsulation) نظيف، بتخسر إن debugging بيبقى أصعب لإن القيمة مش ظاهرة في الكود مباشرة.
- التتبّع: لو عندك Closures متداخلة كتير، فهم مين ماسك مين بيبقى متعب. بتكسب قوة، بتخسر بساطة القراءة.
- الأداء: إنشاء Closure أغلى شوية من دالة عادية، بس الفرق مهمل (ميكروثواني) إلا لو بتعمل ملايين منهم في loop ضيق.
متى لا تستخدم Closures
لو محتاج تشارك state بين أجزاء كتير من البرنامج، الـ Closure غلط — ده شغل module أو class أو state management مركزي. كمان لو بتعمل Closures جوه loop بيلفّ ملايين المرات وبتخزّن فيها بيانات كبيرة، ممكن تعمل تسريب ذاكرة (Memory Leak): المتغيّرات بتتراكم ومتتمسحش، والرام بتطلع لفوق لحد ما السيرفر يقع. في الحالة دي صفّر المراجع يدوياً بـ ref = null بعد ما تخلص.
الخطوة التالية
افتح console المتصفح دلوقتي والصق دالة createCounter اللي فوق. ناديها مرتين في متغيّرين مختلفين، وشوف بنفسك إزاي كل عدّاد مستقل. لو القيم اتخلطت مع بعض، يبقى عندك var مكان let في مكان ما — دوّر عليها.
المصادر
- تعريف الـ Closure والبيئة المعجمية: توثيق MDN Web Docs من Mozilla — "Closures".
- مفهوم Lexical Scoping و let/const: مواصفة ECMAScript 2015 (ES6) الرسمية من TC39.
- سلوك var مقابل let في الحلقات: توثيق MDN — "let" و"var".