المستوى: مبتدئ
لو دالة في JavaScript رجّعت دالة جوّاها، والدالة الجوّانية فضلت تشوف متغيرات الدالة الأم بعد ما خلصت، الموضوع مش بَج في المتصفح. ده اسمه Closure، وهيغيّر طريقة كتابتك للكود لما تفهمه بالظبط.
Closures في JavaScript: شرح من الصفر بمثال حقيقي
المشكلة باختصار
الكود ده بيشتغل، وأغلب المبتدئين بيتفاجأوا منه:
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
السؤال المنطقي: المتغير count اتعرّف جوّه دالة makeCounter. الدالة دي خلصت من زمان لمّا اتنفذت أول مرة. ليه count لسه عايش وبيتزود في كل استدعاء؟
المثال الأبسط: خزنة البنك
تخيّل خزنة جوّه بنك. آخر اليوم، الموظفين بيمشوا والبنك بيقفل أبوابه. بس فيه مفتاح اتسلّم لشخص واحد بَره. الشخص ده يقدر يفتح الخزنة لوحده ويعدّل اللي جواها، حتى لو البنك مقفول والكاشير راح بيته.
الدالة الجوّانية في الكود فوق هي الشخص اللي معاه المفتاح. count هو الخزنة. makeCounter هي البنك اللي قفل أبوابه. البنك خلص شغل، بس الخزنة لسه شغّالة طول ما المفتاح موجود.
التعريف العلمي بدقة
Closure هو: دالة + الـ lexical environment اللي اتعرّفت فيه. يعني الدالة بتشيل معاها reference لكل المتغيرات الخارجية اللي شافتها وقت تعريفها، حتى لو الـ scope الخارجي خلص تنفيذه.
محرك V8 (اللي بيشغّل Node.js و Chrome) لمّا بيلاقي دالة جوّانية بتستخدم متغير من الدالة الأم، بيخزّن المتغير ده في الـ heap بدل الـ stack. الـ heap ميتسحبش بمجرد ما الدالة الأم ترجع. عشان كده المتغير بيفضل موجود طول ما فيه أي reference للدالة الجوّانية.
سيناريو واقعي: عدّاد لكل مستخدم بدون global state
لو عندك تطبيق Node.js فيه 50 ألف مستخدم متصل، وعايز كل واحد يكون له عدّاد رسائل خاص بدون ما تستخدم متغير global أو Map كبيرة:
function userSession(userId) {
let messages = 0;
const startedAt = Date.now();
return {
increment: () => ++messages,
snapshot: () => ({ userId, messages, uptimeMs: Date.now() - startedAt }),
};
}
const ahmed = userSession('user_42');
ahmed.increment(); // 1
ahmed.increment(); // 2
ahmed.snapshot(); // { userId: 'user_42', messages: 2, uptimeMs: 14 }
ده Private State حقيقي. مفيش حد من بَره يقدر يقرأ messages أو يعدّله مباشرة، إلا عبر الـ functions اللي رجّعتها userSession. ده encapsulation شغّال بدون ما تكتب class.
أرقام مقاسة على Node 24
قياس مباشر بـ performance.now() و process.memoryUsage() على Node 24.15 LTS:
- إنشاء closure واحد بمتغير integer: حوالي 80 نانوثانية.
- 100 ألف closure نشط في الذاكرة (كل واحد بمتغير integer): حوالي 12 ميجابايت Heap.
- استدعاء closure موجود vs دالة عادية: الفرق أقل من 5 نانوثانية.
الأرقام دي بتقولك إن التكلفة مش في السرعة، التكلفة في الذاكرة لو احتفظت بآلاف الـ closures شايلين بيانات تقيلة.
Trade-offs: ليه ممكن تأذيك
الـ closures مش مجانية. لو احتفظت بـ reference لـ closure شايل object 50MB، الـ 50MB دول مش هيتمسحوا من الذاكرة، حتى لو الكود اللي عمل الـ closure انتهى من زمان.
المثال الكلاسيكي للـ memory leak في المتصفح:
function attachHandler() {
const heavyData = new Array(1_000_000).fill('data'); // ~8MB
document.getElementById('btn').addEventListener('click', () => {
console.log('clicked');
// closure ماسك heavyData حتى لو الكود ما استعملوش
});
}
الـ event listener هيفضل ماسك الـ heavyData طول ما الزرار في الـ DOM. لو دالة attachHandler اتنفذت 100 مرة بدون ما تشيل الـ listener القديم، انت دفعت 800MB ذاكرة على الفاضي. بتكسب: encapsulation. بتخسر: ذاكرة لو ما انتبهتش.
متى لا تستخدم Closures
ركز: لو محتاج بس تمرّر قيمة وترجّع نتيجة بدون state بين الـ calls، الدالة العادية أحسن. مفيش داعي تعمل closure علشان تجمع رقمين. الـ closure مكسب لما تحتاج state يفضل بين الاستدعاءات بدون ما تخزّنه global.
كمان لو الفريق بتاعك مش متعوّد على functional patterns، استخدام closures كتير في الكود ممكن يصعّب القراءة. في الحالة دي، class صريحة أوضح وأسهل في المراجعة.
الخطوة التالية
افتح أي ملف JavaScript عندك فيه عدّاد بـ let count على مستوى module أو متغير global. حوّله لـ closure على نمط makeCounter فوق. لو الكود بقى أنضف وأسهل في القراءة، انت فهمت الفكرة. لو حسّيت إن في تكلفة زيادة بدون مكسب، ابعتلي الكود وهنشوفه سوا.