Closures — الحقيبة اللي بتفضل معاك
ليه الدرس ده؟ من درس 0007 (سيناريو C) وأنت شايف obj بيفضل موجود بعد ما الدالة اللي جواها خرجت من الـ stack، والسؤال اتفتح عمداً من غير إجابة. النهارده بنقفله نهائياً — والإجابة هتفسّرلك حاجات كتير كنت شغّلتها من غير ما تعرف السبب.
🔁 مراجعة سريعة قبل ما تبدأ (~5 دقايق):
- افتكر سيناريو C في محاكي 0007 — تحديدًا آخر ملاحظة فيه: "إزاي obj فضل موجود بعد ما runLater خلصت؟"
- اسأل نفسك من الذاكرة: سلسلة البحث (0003) بتمشي إزاي؟ ولما دالة تتعرّف جوه دالة تانية، بتشوف متغيرات مين؟ — طلعت بسهولة؟ ادخل. لأ؟ راجع محاكي 0003 الأول.
١ — الفكرة في 3 جمل
① أي دالة بتتعرّف جوه دالة تانية بتتولد ومعاها رابط دايم بمتغيرات الدالة الخارجية — زي حقيبة 🎒 شايلاها معاها لأي مكان تمشيله.
② لما الدالة الخارجية تخلص وتخرج من الـ stack (زي أي دالة عادية)، متغيراتها مش بتتمسح طالما لسه فيه حاجة شايلة الرابط ده — الحقيبة بتفضل حية في الذاكرة بره الـ stack.
③ الاسم الرسمي للرابط ده هو closure — مش ميزة خاصة تفعّلها، هي نتيجة طبيعية لإزاي JS بيتعامل مع الدوال والـ scope اللي إنت عارفهم بالفعل من 0003.
📌 وقفة تسمية مهمة: "سلسلة البحث" اللي اتعلمتها في 0003 ليها اسم رسمي بالإنجليزي هتقابله كتير في التوثيق والانترفيوهات: Lexical Scope. "Lexical" معناها "بالكتابة" — يعني نطاق الدالة بيتحدد من مكان كتابتها في الكود، مش من مكان أو وقت ما بتتنادى منه (بالظبط عكس الـ this، فاكر 0008: "scope بالكتابة، this بالنداء").
يعني الـ closure مش موضوع منفصل عن الـ lexical scope — هو نتيجته المباشرة: بما إن نطاق الدالة اتحدد نهائيًا وقت كتابتها (مش وقت نداءها)، الرابط ده بيفضل صحيح وشغال حتى لو الدالة الخارجية خلصت من زمان. الاسمين بيوصفوا نفس الآلية من زاويتين: Lexical Scope = القاعدة (فين بتدوّر)، Closure = النتيجة (إن الدوّرة دي بتفضل شغالة حتى بعد ما مصدرها يختفي من الـ stack).
٢ — مثال أساسي: عداد بيتذكر نفسه
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter1 = makeCounter();
console.log(counter1());
console.log(counter1());
console.log(counter1());
لاحظ: makeCounter() اتنفذت مرة واحدة بس وخلصت. بس الدالة اللي رجعتها لسه قادرة توصل لـ count وتزودها — رغم إن الـ frame بتاع makeCounter اتشال من الـ stack من زمان. الناتج الحقيقي:
1 2 3
✓ متحقق بتشغيل Node.js فعلي (2026-08-14). وكمان: typeof count من برة الدالة بيرجع undefined — يعني count محمية تمامًا، مفيش وصول مباشر ليها من أي حتة تانية.
٣ — اللغز المعلّق من 0007: اتحل رسمياً
ده نفس الكود اللي شفته في سيناريو C من 0007 (مبسّط شوية، من غير جزئية تبديل الأوبجكت اللي حللتها وقتها):
let user = { name: "عمر", hi() { console.log("أهلاً " + this.name); } };
function runLater(obj) {
setTimeout(() => obj.hi(), 100);
}
runLater(user);
اللغز: runLater بتخلص وترجع فورًا (frame بتاعها بيتشال من الـ stack على طول)، قبل ما الـ 100ms تعدي بكتير. طب إزاي الـ arrow اللي جوه setTimeout لسه عارفة obj لما تيجي تتنفذ بعد كده؟
الإجابة دلوقتي واضحة: الـ arrow function اتعرّفت جوه runLater، فاتولدت ومعاها 🎒 حقيبة فيها رابط بـ obj (سلسلة البحث بتاعتها بتوصل لبيئة runLater، بنفس آلية 0003). لما runLater خلصت، الـ frame اتشال — بس الحقيبة فضلت حية لأن الموعد المسجل لسه شايلها. لما الموعد يجي، الـ arrow بتفتح الحقيبة وتلاقي obj زي ما هي.
✓ نفس الكود شغال ومتحقق منه: "أهلاً عمر" بتطبع بعد الـ 100ms بالظبط زي ما كان في 0007.
٤ — شوفها بعينك: الحقيبة في المحاكي
راقب البانل الأخضر المتقطع 🟩 — ده مش الـ Call Stack، ده مكان جديد لـالبيئات الباقية (بره الـ stack تمامًا). لما frame يتشال من 🟦 ومتغيراته لسه محتاجة، هتشوفها بتتحول لهناك. توقع قبل كل خطوة!
function makeCounter() { let count = 0; return function () { count++; return count; }; } const counter1 = makeCounter(); counter1(); counter1();
٥ — نقطة دقيقة: الحقيبة شايلة المتغير نفسه، مش نسخة منه
سؤال مهم: الـ closure بياخد قيمة المتغير وقت ما الدالة اتعرّفت، ولا بيفضل شايف المتغير نفسه حتى لو اتغيّر بعدين؟
function makeGreeter() {
let name = "عمر";
const greet = () => console.log("أهلاً " + name);
name = "سارة";
return greet;
}
const greet = makeGreeter();
greet();
لاحظ: name اتغيّرت لـ"سارة" بعد ما greet اتعرّفت، وقبل أي نداء عليها. الناتج الحقيقي:
أهلاً سارة
✓ متحقق بتشغيل Node.js فعلي (2026-08-14). يعني الحقيبة مش بتاخد "صورة" للقيمة وقت التعريف — هي بتفضل شايفة نفس المتغير الحي، بنفس آلية سلسلة البحث العادية (0003). ده بالظبط نفس المبدأ اللي شفته في تبديل الأوبجكت بسيناريو C من 0007.
٦ — توقع قبل ما تشوف (3 تمارين)
function makeAdder(base) {
return function (n) { return base + n; };
}
const addFive = makeAdder(5);
console.log(addFive(0).toString());
const a = makeCounter(); const b = makeCounter(); a(); a(); console.log(b());
function makeGreeter() {
let name = "عمر";
const greet = () => console.log("أهلاً " + name);
name = "سارة";
return greet;
}
makeGreeter()();
٧ — اختبر نفسك
س1: الـ closure بالتعريف الدقيق هو إيه؟
س2: ليه makeCounter() بترجع عداد مستقل كل مرة تتنادى؟
س3: لو غيّرت متغير بعد ما الـ closure اتعرّفت وقبل ما تتنادى، الـ closure هتشوف القيمة القديمة ولا الجديدة؟
س4 (سؤال judgment): امتى تستخدم closure فعليًا في شغلك؟
٨ — التحدي الأخير: صمّم حساب بنكي محمي (من غير AI!)
المطلوب: دالة (factory function) اسمها createAccount(initialBalance) بترجع أوبجكت فيه دالتين بس:
deposit(amount)— بتزود الرصيد وترجعه.getBalance()— بترجع الرصيد الحالي.
الشرط الجوهري: الرصيد نفسه (المتغير) لازم يكون مستحيل الوصول ليه مباشرة من برة — يعني لو حد كتب account.balance، لازم يرجع undefined. اختبر ده بنفسك بعد ما تكتب الحل.
🎁 بونص لو حابب: ضيف withdraw(amount) بشرط إنها ترفض السحب لو المبلغ أكبر من الرصيد المتاح.
٩ — المصدر الأساسي للتعمق
- javascript.info — Variable scope, closure — الشرح المرجعي لآلية الـ Lexical Environment.
- MDN — Closures — المرجع الرسمي مع أمثلة إضافية (private methods، counters).
- ورقة المراجعة: reference/js-execution-model.html