المستوى: مبتدئ
لو لقيت دالة جوّانية في JavaScript بتقرأ متغير من دالة خارجية خلصت تنفيذها — مفروض المتغير ده اتمسح، لكنه شغّال. ده بالظبط الـ closure. المقال هيخليك تفهمه في 7 دقائق بمثال كود قصير، تعريف علمي دقيق، حالات الاستخدام، وحدود الأمان.
Closures في JavaScript: قاعدة واحدة بتفسّر كل اللخبطة
المشكلة باختصار
كل مبتدئ بيقابل الكود ده ويتلخبط:
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3السؤال اللي بيتسأل في كل مقابلة عمل: ليه count لسه عايش؟ makeCounter خلصت ومفروض الـ scope بتاعها انمحى. لكن الرقم بيزيد كل مرة. السبب هو الـ closure.
المثال قبل التعريف العلمي
تخيّل درج صغير في مكتب فيه ورقة مكتوب عليها رقم. لما الموظف بيغادر المكتب، الدرج ما بيتحرقش. بيفضل في مكانه. لو الموظف ساب لزميله مفتاح الدرج قبل ما يمشي، الزميل يقدر يفتحه ويعدّل الرقم اللي جوّاه أي وقت. لو رمى المفتاح، الدرج بيتحرق فعلًا (الذاكرة بتترجع للنظام).
في الكود اللي فوق:
- الدرج = الـ scope بتاع
makeCounter. - الورقة جوّاه = المتغير
count. - المفتاح = الدالة المرجوعة (المخزّنة في
counter). - الموظف اللي مشي = استدعاء
makeCounterاللي خلص.
طول ما المفتاح موجود، الدرج ما يتحرقش. ده هو الـ closure حرفيًا.
التعريف العلمي الدقيق
الـ closure هو ربط بين دالة وبيئتها المعجمية (lexical environment) وقت إنشائها. كل دالة في JavaScript بتحفظ مرجع داخلي اسمه [[Environment]] بيشاور على الـ scope اللي اتعرّفت فيه. لما الدالة بتنفّذ، بتتعمل لها بيئة جديدة بتشاور على الـ [[Environment]] ده كأبوها.
طول ما الدالة لسه قابلة للوصول (reachable) من أي مكان في البرنامج، الـ Garbage Collector ما يقدرش يحرّر الـ scope اللي هي شايفاه. ده مش سحر، ده ECMAScript Spec — بالظبط القسم 9.2 (Function Environment Records).
مثال أقوى: مولّد دوال
function multiplier(factor) {
return function (num) {
return num * factor;
};
}
const double = multiplier(2);
const triple = multiplier(3);
double(5); // 10
triple(5); // 15
double(10); // 20ركّز هنا: كل استدعاء لـ multiplier بيخلق scope جديد مستقل بمتغير factor خاص بيه. double شايف factor = 2 للأبد. triple شايف factor = 3 للأبد. لو خلقت 1000 multiplier، عندك 1000 scope مستقل في الذاكرة.
4 حالات استخدام حقيقية
- إخفاء حالة بدون class: بدل ما تسيب متغير global لأي حد يلمسه، اعمله جوه closure. الوصول الوحيد للقيمة هيكون عن طريق دالة محددة.
- callbacks بمعلومات إضافية: زرار "Delete" في React محتاج يعرف
idالعنصر. بدل ما تمرره في كل render، تستخدم closure:onClick={() => handleDelete(item.id)}. - once / debounce / throttle: دوال بتحتاج تتذكّر آخر مرة اتنادت فيها أو هل اتنادت قبل كده. الذاكرة دي بتعيش جوّا closure.
- currying وpartial application: تجزئة دالة بحجج كتيرة لدوال صغيرة متتالية. كل جزء بيقفل (close over) على الحجة اللي اتمررت ليه.
مثال عملي: once في 6 سطور
function once(fn) {
let called = false;
let result;
return function (...args) {
if (called) return result;
called = true;
result = fn.apply(this, args);
return result;
};
}
const init = once(() => console.log("started!"));
init(); // started!
init(); // (مفيش طباعة)
init(); // (مفيش طباعة)المتغيران called وresult عايشين جوّا closure. مفيش طريقة تلمسهم من برّا. ده مش تنظيم بس، ده encapsulation فعلي.
الـ Trade-offs اللي لازم تعرفها
الـ closure مش مجاني. الافتراض إن كل closure بيمسك بـ scope كامل في الذاكرة، حتى لو محتاج متغير واحد بس. لو حفظت 100 ألف closure على عناصر DOM (مثلًا event listeners في table كبيرة)، احتمال تعمل memory leak كبير.
أرقام تقريبية: closure واحد على أقل من 5 متغيرات بيكلّف بين 200 و600 byte إضافية في heap. ده عادي. لكن لو الـ scope الخارجي فيه array طوله مليون عنصر، الـ array كله بيفضل في الذاكرة طول ما الـ closure حي. ده الفخ الأساسي.
المكسب: encapsulation حقيقية، state private، كود أقصر بدون classes. التكلفة: ذاكرة زيادة وأحيانًا debugging أصعب لأن المتغيرات مش ظاهرة في الـ console.
متى لا تستخدم Closures
- لو الحالة لازم تكون متاحة لأكتر من module: الـ closure بيقفل المتغير جوّاه. لو محتاج تشاركه، استخدم class أو state management library (Redux, Zustand, Pinia).
- لو فاكر إنها حماية لبيانات حساسة: ده مش أمان. أي حد عنده DevTools يقدر يقرأ الـ heap. الـ closure تنظيم مش تشفير.
- لو في حلقة كبيرة بتنشئ آلاف الدوال: راجع لو فعلًا محتاج closure لكل عنصر، أو تقدر تستخدم دالة واحدة مشتركة بحجج.
- لو بتشتغل على hot path في performance-critical code: استدعاء دالة لها closure أبطأ شوية من دالة pure بدون مراجع خارجية. الفرق ميكروثواني، لكنه بيفرق في loops على ملايين العناصر.
الخطوة التالية
افتح console المتصفح دلوقتي ونفّذ السطر ده:
const counter = (function () { let n = 0; return () => ++n; })();
counter(); counter(); counter(); // 1, 2, 3لو الرقم زاد، الـ closure شغّال على جهازك. بعد كده اعمل counter تاني واتأكد إنه مستقل عن الأولاني — ده بالظبط اللي بيخلي الـ closures أقوى من الـ globals.
المصادر
- MDN Web Docs — Closures: developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
- ECMAScript 2024 Language Specification — Section 9.2 Function Environment Records: tc39.es/ecma262
- You Don't Know JS Yet (Kyle Simpson) — Scope & Closures: github.com/getify/You-Dont-Know-JS
- V8 Blog — Memory considerations for closures: v8.dev/blog