لو في حاجتين بيخلوا أي JavaScript Developer يهرش في دماغه، فهما this والـ Closures.
إحنا قتلنا this بحثًا في المقالة اللي فاتت، والنهاردة جه الدور على البعبع التاني: الـ Closure.
الـ Closure هو السحر اللي بيخلي لغة زي JavaScript مميزة جدًا (ومربكة جدًا). هو السبب إننا بنقدر نعمل حاجات عظيمة زي Module Pattern (إننا نقسم الكود لأجزاء صغيرة ومعزولة) والـ Memoization (إننا نخلي الدالة تفتكر نتايج حساباتها القديمة عشان متهنجش)، وهو برضه السبب إننا ممكن نوقع السيرفر بـ Memory Leak (تسريب في الذاكرة) لو مش فاهمين إحنا بنعمل إيه.
المقالة دي مش شرح نظري ممل. دي رحلة جوه الـ “Backpack” بتاع الدالة. هنفصص كل مصطلح عشان تخرج من هنا فاهم الـ Engine شغال إزاي بجد، ومعاك التريكات اللي تميزك كـ Senior.
1. التعريف الأكاديمي (اللي محدش بيفهمه)
لو فتحت MDN هتلاقي التعريف ده:
“A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).”
كلام كبير ومحتاج ترجمة؟ تعال نفك الشفرة كلمة كلمة.
يعني إيه Lexical Environment؟
كلمة Lexical يعني “كتابي” أو “نصي”. والـ Environment يعني البيئة المحيطة. ببساطة: مكان كتابة الكود بتاعك فين في الملف؟
تخيل الـ Scope كأنه مبنى إزاز “فيميه” (Tinted Glass):
- اللي جوه (Inner Function) يقدر يشوف كل اللي بره.
- اللي بره (Outer Function) مايشوفش اللي جوه.
الـ JS بتهتم جدًا بـ “مكان الولادة”. لو دالة اتكتبت جوه دالة تانية، فهي شايفة كل حاجة حواليها في المكان ده. ده قانون ثابت مبيتغيرش، وده اللي بنسميه Lexical Scope.
إذن، الـ Closure باختصار: هو إن الدالة “تفتكر” المكان اللي اتولدت فيه (Lexical Environment)، حتى لو المكان ده اتمسح من الوجود!
2. شنطة الظهر (The Backpack Analogy)
عشان تفهم إزاي الدالة “بتفتكر”، تخيل الـ Function عاملة زي التلميذ وهو مروح من المدرسة.
- المدرسة/الفصل: هي الدالة الخارجية (Outer Function).
- التلميذ: هو الدالة الداخلية (Inner Function).
- السبورة والأدوات: هي المتغيرات (Variables) اللي جوه الدالة الخارجية.
التلميذ ده (الدالة Inner) خارج من الفصل (Outer). وهو خارج، خد معاه شنطته (The Backpack). الشنطة دي حاطط فيها نسخة حية (Live Reference) من كل المتغيرات اللي هو محتاجها من الفصل.
حتى لو الفصل اتهد (الدالة Outer خلصت تنفيذ وانتهت)، التلميذ لسه ماشي بالشنطة، وجواها كل حاجة كانت موجودة وقت خروجه.
function outer() {
let secret = "كلمة السر هي 123"; // ده متغير جوه الفصل
function inner() {
console.log(secret); // التلميذ بيستخدم حاجة من الفصل
}
return inner; // التلميذ روح بيته (خرج بره الدالة outer)
}
const myFunc = outer(); // outer اشتغلت وخلصت وماتت!
myFunc(); // "كلمة السر هي 123" 🤯
السؤال اللولبي: إزاي myFunc قدرت تطبع secret مع إن outer خلصت تنفيذ والـ Scope بتاعها المفروض يكون اتشال من الـ Memory؟
الإجابة: الـ Closure Backpack. لما inner رجعت، مرجعتش ايدها فاضية. رجعت بشنطة (Closure) مخزنة فيها لينك مباشر لـ secret.
3. شوف بعينك (Debug Trick) 🕵️♂️
عايز تتأكد إن الكلام ده حقيقي ومش سحر؟ افتح الـ Chrome Console وجرب التريكاية دي:
// اكتب الكود اللي فوق في الكونسول
console.dir(myFunc);
لما تفتح السهم بتاع الدالة، دور على خاصية اسمها [[Scopes]]. هتلاقي جواها:
Closure (outer): وجواهsecret: "كلمة السر هي 123".Global: اللي فيهwindowوباقي الحاجات.
أهي “الشنطة” قدام عينك أهي! الـ Engine محتفظ بيها ومسميها Closure رسميًا.
4. ما وراء الستار: Heap vs Stack (الذاكرة بالتفصيل)
عشان تفهم “السحر” ده حصل إزاي فيزيائيًا في الرامات، لازم نفهم الـ JS Engine بيخزن البيانات فين. الذاكرة عندنا نوعين:
- Stack (الرفوف السريعة): دي ذاكرة مؤقتة وسريعة جدًا. بتخزن الحاجات البسيطة (Primitives) وتنفيذ الدوال. مجرد ما الدالة تخلص، الـ Stack Frame بتاعها (صفحتها) بتتقطع وتترمي.
- Heap (المخزن الكبير): دي ذاكرة كبيرة ودائمة، بنخزن فيها الحاجات التقيلة زي الـ Objects والـ Arrays… وكمان الـ Closures!
السيناريو اللي حصل كالتالي:
طبيعيًا، لما دالة
outerبتخلص، كل المتغيرات اللي في الـ Stack بتاعها بتتمسح.لكن هنا، الـ JS Engine (الذكي) لاحظ حاجة غريبة:
“استنى! في دالة داخلية
innerلسه عايشة، والدالة دي بتستخدم متغيرsecretمن الدالة الأم.”فالـ Engine قال: “لو مسحت
secretمن الـ Stack، الدالةinnerهتبوظ.”الحل؟ الـ Function Object بتاع
innerوالـ Environment Record (السجل اللي فيه المتغيرات) بيتحفظوا في الـ Heap Memory.ملحوظة دقيقة:
- الـ Function Object نفسه في الـ Heap.
- الـ Closure Scope (Environment Record) كمان في الـ Heap.
- والاتنين مربوطين ببعض بـ Reference بيمنعهم من المسح.
5. آلة الزمن (Time Travel Capability)
أقوى ميزة للـ Closure هي إنه بيوقف الزمن بالنسبة للمتغيرات. لما تعمل Ajax Request أو setTimeout، الكود بيكمل جري، بس الـ Closure بيخلي الدالة اللي هتتنفذ بعدين (الـ Callback) قادرة تشوف البيانات اللي كانت موجودة وقت ما طلبت الطلب.
function askForPizza(flavor) {
setTimeout(function() {
console.log(`Here is your ${flavor} pizza! 🍕`);
}, 3000);
}
askForPizza("Pepperoni");
بعد 3 ثواني، دالة askForPizza خلصت من زمان، بس الـ Callback لما صحيت من النوم، لقت flavor محفوظة في الشنطة بتاعتها. ده حرفيًا Time Travel.
6. استخدامات عملية (ليه وجع الدماغ ده؟)
إحنا مش بنتعلم ده عشان نجاوب في الانترفيو بس، الـ Closures هي العمود الفقري لـ Patterns (أنماط برمجية) بنستخدمها كل يوم.
أ) Data Privacy (الكبسولة)
في لغات زي Java، تقدر تقول المتغير ده private يعني محدش يشوفه غير الكلاس ده. في الـ JS زمان مكنش فيه كده. فكنا بنتحايل على الموضوع بـ Closures.
عايزين نعمل محفظة wallet، محدش يقدر يلعب في الرصيد بتاعها مباشر.
function createWallet(initialBalance) {
let balance = initialBalance;
return {
deposit: function(amount) {
balance += amount;
console.log(`Tamam! New balance: ${balance}`);
},
getBalance: function() {
return balance;
}
};
}
const myWallet = createWallet(100);
myWallet.deposit(50); // Tamam! New balance: 150
console.log(myWallet.getBalance()); // 150
console.log(myWallet.balance); // undefined 🛡️ (محدش يقدر يوصل للمتغير نفسه)
ب) Event Listeners (تفاعل الصفحة)
مثال واقعي جدًا للـ Closure هو الـ Event Listeners.
function setupButton() {
let clickCount = 0;
document.getElementById('btn').addEventListener('click', function() {
clickCount++;
console.log(`تم الضغط ${clickCount} مرة`);
});
}
// كل ضغطة هتزود العداد لأن الدالة محتفظة بـ clickCount
ده مثال قوي للـ Closure، وبرضه مصدر مشهور للـ Memory Leaks لو نسينا نشيل الـ Listener لما العنصر يتمسح من الصفحة.
ج) React Hooks
لو بتكتب React، فـ useState و useEffect هما حرفيًا مبنيين على فكرة الـ Closures. لما بتكتب const [count, setCount] = useState(0)، الـ React بيخزن الـ count في Closure خاص بالـ Component Instance بتاعك. عشان كده لما الـ Component يترسم تاني (Re-render)، الـ count مبيتمسحش، لأنه محفوظ في “شنطة” React ماسكها بره الـ Component.
7. الجانب المظلم: Memory Leaks
زي ما الـ Closure مفيد، هو ممكن يكون كارثة لو استخدمته غلط. فاكر الشنطة؟ تخيل لو التلميذ وهو مروح، بدل ما ياخد كراسة في شنطته، خد تختة الفصل كلها!
هنا بيجي دور الـ Garbage Collector (جامع القمامة). ده عامل نظافة شغال في الـ JS Engine، وظيفته يلف كل شوية يشوف أي داتا محدش بيستخدمها ويمسحها عشان يفضي الرامات.
المشكلة في الـ Closure إنه بيقول لعامل النظافة: “سيب الداتا دي! أنا ماسك فيها ومحتاجها.” لو الداتا دي كبيرة جدًا (مثلاً صورة كبيرة، أو Array فيه ملايين العناصر)، والـ Closure ده فضل عايش ومش بيتمسح… الداتا دي هتفضل في الـ RAM للأبد.
مثال للـ Leak الحقيقي:
function realLeak() {
const hugeData = new Array(1000000).fill("🔥"); // داتا عملاقة
return function() {
console.log(hugeData.length); // دلوقتي بنستخدمها فعلاً! فالـ Closure محتفظ بيها
};
}
const leakyFunc = realLeak();
توضيح: لو مكناش استخدمنا
hugeDataجوه الدالة، الـ Engines الحديثة (زي V8) كانت هتعمل Optimization وتشيلها من الذاكرة. بس طالما استخدمناها، الـ Closure هيفضل ماسكها في الـ Heap ومش هتتمسح أبدًا طول ماleakyFuncموجودة.
شرط الـ Leak الحقيقي:
- Closure ماسك الداتا.
- Refernece طويل العمر (زي
leakyFuncاللي عايش في الـ Global ومش بيتمسح).
الخلاصة
الـ Closure مش سحر أسود، هو ميكانيزم ذكي جدًا من الـ JS عشان تحافظ على البيانات اللي الدوال محتاجاها.
- Lexical Scope: “ابن مين ومكتوب فين؟” مكان الكتابة بيحدد الدالة شايفة إيه.
- Backpack: الدالة بتشيل معاها (في شنطة خفية) المتغيرات اللي حواليها وهي راجعة، عشان متنساش أصلها.
- Heap: المتغيرات دي بتعيش في المخزن الدائم.
- Time Travel: القدرة على الاحتفاظ بقيم للمستقبل (للـ Callbacks).
افهم الـ Closure صح، وهتلاقي نفسك بتكتب كود Functional وأنضيف بمراحل، وهتبطل تستخدم Global Variables خالص.
سؤال الـ var vs let الكلاسيكي: لو عملنا Loop وجواها
setTimeout، ليه النتيجة بتختلف؟// مع var (غلط ❌) for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // هيطبع 3, 3, 3 }, 1000); } // مع let (صح ✅) for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // هيطبع 0, 1, 2 }, 1000); }السبب:
varبتعمل Scope واحد مشترك للدالة كلها، فكل الـ iterations بيشاوروا على نفس المتغيرiاللي قيمته بقت 3 في الآخر. أماletفبتعمل New Environment لكل لفة، فكل دالة بتاخد نسخة خاصة بيها منi.
