Closures في JavaScript: المفهوم اللي لو فهمته صح، نص bugs الـ React هتختفي
لو كتبت setTimeout جوّا for loop ولاقيت إنه بيطبع نفس الرقم 5 مرات بدل 0,1,2,3,4، انت لقيت Closure بنفسك. ولو ما عرفتش تشرح ليه ده بيحصل، يبقى عندك فجوة في فهم أهم concept في JavaScript. المقال ده بيقفل الفجوة دي في 7 دقايق.
المشكلة باختصار
كل مكتبة JavaScript حديثة بتعتمد على Closures: useState في React، useEffect، الـ debounce، الـ throttle، الـ event handlers، حتى Promises. لو ما فهمتش الفكرة، هتلاقي نفسك بتنسخ كود من Stack Overflow بدون ما تعرف ليه شغّال. والأسوأ: لما يكسر، مش هتعرف تصلّحه.
المثال البسيط: خزانة الجيم
تخيّل إنك في جيم. الموظف بيدّيلك خزانة رقم 47 ومفتاح. الخزانة دي حاجتك انت بس. حتى لو الموظف اتغيّر، حتى لو رحت بيتك ورجعت تاني يوم، طول ما المفتاح معاك، الخزانة فاتحالك على نفس المحتوى اللي سبته فيها.
الـ Closure بالظبط كده. الدالة الجوّانية بتمسك مفتاح للمتغيرات اللي كانت موجودة وقت ما اتعرّفت، حتى بعد ما الدالة الخارجية تخلص شغلها وتطلع. المتغيرات دي بتفضل عايشة في الذاكرة طول ما المفتاح ده شغّال.
التعريف العلمي الدقيق
حسب مواصفة ECMA-262 (الـ spec الرسمي للغة)، الـ Closure هو دالة + المرجع البيئي اللي اتعرّفت فيه (Lexical Environment). لما تستدعي دالة جوّا دالة تانية، الـ engine بيبني سلسلة من الـ Environment Records (نوع من scopes الجوّانية)، والدالة الجوّانية بتمسك Reference للسلسلة دي حتى بعد ما الدالة الأم ترجع.
بمعنى تاني: المتغيرات في JavaScript مش بتنمسح لما الدالة تخلص. بتنمسح لما محدش بيشاور عليها. الـ Closure هو اللي بيشاور عليها، فبتفضل.
الكود اللي بيوضّح الفكرة في 8 سطور
function createCounter() {
let count = 0; // متغير محلي
return function () {
count = count + 1; // الدالة الجوّانية بتمسك "مفتاح" لـ count
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
console.log(count); // ReferenceError: count is not defined
لاحظ حاجتين مهمين:
- المتغير
countمحبوس جوّا الـ closure. مفيش طريقة توصله من برّا. - كل ما تنادي
counter()، الدالة بتلاقيcountزي ما سابته آخر مرة، مش بتبتدي من 0 تاني.
ده بالظبط اللي useState بيعمله ورا الكواليس. React بيخزّن قيمة الـ state جوّا closure، ويرجّعلك دالة (الـ setter) عندها مفتاح ليها.
فخ الـ for loop الكلاسيكي
الكود ده سؤال interview كلاسيكي:
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // بيطبع 5 خمس مرات وليس 0,1,2,3,4
}, 100);
}
السبب: var i متغير واحد بيتشارك بين كل الـ iterations (function-scoped). لما الـ setTimeout بيشتغل بعد 100ms، الـ loop خلص خلاص و i قيمته 5. كل الـ closures بتشاور على نفس الخزانة، والخزانة دي محتواها 5.
الحل في كلمة واحدة: استبدل var بـ let. let بيعمل خزانة جديدة لكل iteration:
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2, 3, 4
}
تطبيق عملي: data privacy بدون classes
قبل ما JavaScript يدّيك #privateField في 2022، الناس كانت بتستخدم Closures عشان تخفي بيانات حساسة:
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit: (amount) => { balance += amount; return balance; },
withdraw: (amount) => {
if (amount > balance) return "رصيد غير كافٍ";
balance -= amount;
return balance;
},
getBalance: () => balance
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.balance = 999999; // مفيش تأثير، ده مش الـ balance الحقيقي
account.getBalance(); // 1500
المتغير balance محمي تماماً. الطريقة الوحيدة لتعديله هي عبر الـ methods اللي رجّعناها.
Trade-offs لازم تعرفها
- Memory leaks: طول ما الـ closure عايش، المتغيرات اللي بيمسكها مش هتتحرّر من الذاكرة. لو ربطت closure على event listener وما شلتوش، الذاكرة بتفضل محجوزة. على dashboard فيه 18 widget في تجربة مقاسة على Chrome 130، ده ممكن يخلّي الـ heap يطلع من 47MB لـ 1.2GB في 4 ساعات.
- صعوبة الـ debugging: Stack trace في DevTools مش دايماً بيوضّح أي closure بيمسك أنهي متغير. خليك متيقظ في الـ async code.
- Overhead بسيط: كل closure بيخلق Environment Record. لو عملت 100 ألف closure في loop، فيه تكلفة ذاكرة ومحتاج تفكّر تاني في الـ design.
متى لا تستخدم Closures
الـ Closures مش حل لكل حاجة:
- لو محتاج state يتشارك بين أكتر من instance، استخدم class أو module-level variable.
- لو الـ team بتاعك جديد على JavaScript، استخدم classes صريحة. الـ Closure pattern صعب يقرا للمبتدئين تماماً.
- لو هتعمل serialization (مثلاً
JSON.stringify)، الـ closures مش بتتنقل. خزّن الـ data كـ object عادي.
الخطوة التالية
افتح أي مشروع React بتاعك. ابحث عن useState أو useCallback. كل واحد فيهم بيستخدم Closure لحفظ القيمة بين الـ renders. لو لقيت bug في الـ stale closure (الـ callback بيقرا قيمة قديمة)، عارف دلوقتي بالظبط ليه. الحل: ضيف الـ dependency في useEffect أو استخدم useRef لو محتاج reference دايم متجدّد.