الـ Closure دالة تتذكّر محيطها
في جافا سكريبت، كل مرة تُعرِّف فيها دالة، فهي تحتفظ بهدوء برابط إلى المتغيرات المحيطة بها. وعندما تُنفَّذ هذه الدالة لاحقاً — حتى لو كان ذلك في مكان مختلف تماماً — تبقى قادرة على رؤية تلك المتغيرات. هذا هو الـ closure في جافا سكريبت باختصار.
وإليك أقصر مثال يوضّح الفكرة:
makeGreeter تنفَّذ، رجّعت دالة داخلية، ثم انتهت. طبيعي تتوقع إن المتغير المحلي name راح يختفي بمجرد ما تنتهي الدالة. لكن الدالة الداخلية لا تزال تستخدم name، فتقوم جافا سكريبت بإبقائه حيًّا. greetAda يتذكّر "Ada"، وgreetBoris يتذكّر "Boris". إغلاقان (closures) منفصلان، وقيمتان محفوظتان مستقلّتان.
النطاق الليكسيكي (Lexical Scope) هو أصل الـ closures
القاعدة التي تقف خلف الـ closures في جافا سكريبت تُعرف باسم النطاق الليكسيكي (lexical scope): الدالة ترى المتغيرات الموجودة في المكان الذي كُتبت فيه، لا في المكان الذي استُدعيت منه. وكلمة "ليكسيكي" ببساطة تعني "بحسب موقعها في الكود المصدري".
show يطبع "أنا في الخارج" وليس "أنا داخل caller". والسبب أنها كُتبت بجوار الـ outer الموجودة في النطاق العلوي، فهذه هي النسخة التي تراها. لا يهم أن تستدعيها من مكان آخر يصادف أن لديه outer خاصة به.
الـ closures ليست سوى نطاق ليكسيكي (lexical scope) يعيش أطول من الدالة الخارجية نفسها. المتغير لا يختفي لأن أحدهم ما زال يحتفظ بمرجع إليه.
كل استدعاء يُنشئ closure خاصة به
كل استدعاء جديد للدالة الخارجية يُنشئ متغيرات جديدة، وأي دالة داخلية تُعاد من هذا الاستدعاء تتذكر تلك المتغيرات تحديدًا. ولهذا لم يتعارض greetAda مع greetBoris في المثال السابق.
وأشهر مثال على ذلك هو العدّاد (counter):
الـ a والـ b كلّ واحد منهم عنده نسخته الخاصة من count. ما في شي برّا الدالة المُرجَعة يقدر يلمس هالمتغيرات — count خاص تمامًا. وهاد الشي مو خاصية مفعّلة باللغة، إنما نتيجة طبيعية لطريقة عمل الـ closures.
متغيرات خاصة بدون classes
بما إنه ما في طريقة توصل فيها للمتغيرات المحصورة جوّا إلا عن طريق الدالة المُرجَعة، فيك تستخدم الـ closures لبناء كائنات صغيرة بحالة خاصة (private state) فعلًا:
balance ليست خاصية موجودة على الكائن المُرجَع — بل تعيش داخل الـ closure. الطريقة الوحيدة لقراءتها أو تعديلها هي عبر الدوال التي كشفتها للخارج. صحيح أن الـ classes مع الحقول الخاصة #private تستطيع فعل الشيء نفسه، لكن نسخة الـ closure أقدم منها بعقود ولا تزال منتشرة في كل أرجاء النظام البيئي لجافا سكريبت.
فخ الحلقات الكلاسيكي مع closures
أكثر ما يُربك المطورين في موضوع الـ closures هو استخدامها داخل الحلقات. لاحظ ما يحدث مع var:
كنت تتوقع أن تظهر النتائج 0 و 1 و 2، لكن ما يطبع فعليًا هو 3 و 3 و 3. والسبب؟ المتغير المُعرَّف بـ var يكون نطاقه على مستوى الدالة كلها، أي أنه يوجد متغير i واحد فقط يخدم الحلقة بأكملها. وبالتالي فإن الـ closures الثلاث تمسك بنفس المتغير، وعندما يحين وقت تنفيذها تكون الحلقة قد انتهت وصارت قيمة i تساوي 3.
الحل هو استبداله بـ let:
الآن سيطبع 0 و 1 و 2. الكلمة المفتاحية let نطاقها محدود بالكتلة (block-scoped)، أي أن كل دورة في الحلقة تُنشئ ارتباطًا جديدًا للمتغير i، وبالتالي كل closure يحتفظ بالقيمة الخاصة به. وهذا وحده سبب كافٍ يدفعك لتفضيل let على var.
الـ Closures تلتقط المتغيرات لا القيم
نقطة دقيقة لكنها مهمة جدًا: الـ closure يحتفظ بالإشارة إلى المتغير نفسه، وليس بنسخة من قيمته لحظة تعريف الدالة.
الدالة printMessage تقرأ قيمة message لحظة تنفيذها، لا لحظة إنشائها. إذا أردت التقاط القيمة كما هي في تلك اللحظة، انسخها إلى متغير محلي أولاً — وهذا تقريباً ما يفعله let داخل حلقة for.
نمط شائع من الواقع: دالة تعمل مرة واحدة فقط
إليك أداة صغيرة تستغل الـ closure لضمان تنفيذ الدالة مرة واحدة فقط:
called وresult هنا حالة خاصة (private state) تعيش ما دامت الدالة المُعادة موجودة. لا حاجة لمتغير عام، ولا لكائن إضافي. هذا النمط — دالة مساعدة صغيرة، حالة خاصة، و closure — من أكثر الأشياء فائدة التي تقدّمها جافا سكريبت.
كلمة عن الذاكرة
الـ closure يُبقي المتغيرات التي التقطها حيّةً ما دام هناك شيء يشير إليه. غالبًا هذا ما تريده بالضبط، لكن إذا ربطت closure بشيء طويل العمر (مثل مستمع أحداث في DOM أو كاش عام) وكان يلتقط شيئًا كبير الحجم، فلن يستطيع جامع المهملات (garbage collector) التخلّص منه إلا بعد زوال الـ closure.
function attach() {
const hugeData = new Array(1_000_000).fill("...");
document.addEventListener("click", () => {
console.log(hugeData.length);
});
}
ما دام المستمع (listener) مرتبطًا، سيظل hugeData محفوظًا في الذاكرة. وبمجرد إزالة المستمع (أو تجنّب التقاط ما لا تحتاجه أصلًا)، يختفي هذا المرجع. لست مضطرًا للقلق بشأن هذه التفاصيل كثيرًا، لكن يكفي أن تعرف أن الـ closures والذاكرة مرتبطان ببعضهما.
خلاصة ما تعلّمناه
- الـ closure هو دالة مع المتغيرات التي كانت مرئية لها وقت تعريفها.
- كل استدعاء للدالة الخارجية ينشئ مجموعة جديدة من المتغيرات للدوال الداخلية (closures).
- تتيح لك الـ closures حالة خاصة (private state) دون الحاجة إلى الأصناف (classes).
- داخل الحلقات، استخدم
letحتى تحصل كل دورة على ربط (binding) خاص بها. - الـ closures تلتقط المتغير نفسه، لا قيمته لحظة الإنشاء.
التالي: الكلمة المفتاحية this
الـ closures تتعامل مع المتغيرات المحيطة بالدالة. الجزء التالي يتعلق بـ ما الذي تُستدعى عليه الدالة — وهو في جافا سكريبت يُتحكَّم به عبر this، ويختلف سلوكه اختلافًا كبيرًا عن المتغيرات الملتقطة التي تناولناها للتو.
الأسئلة الشائعة
ما هي الـ Closure في JavaScript؟
الـ Closure ببساطة هي دالة تحتفظ بالمتغيرات من النطاق الذي عُرِّفت فيه، حتى بعد أن ينتهي تنفيذ ذلك النطاق الخارجي. من الناحية التقنية، كل دالة في JavaScript هي Closure، لكن المصطلح يُستخدم عادةً عندما تُعاد دالة أو تُمرَّر إلى مكان آخر وتظل تستعمل متغيرات من نطاقها الأصلي.
لماذا الـ Closures مفيدة؟
لأنها تسمح للدالة بأن تحمل معها حالة خاصة بها (private state). تحصل على بيانات مرتبطة بالدالة دون الحاجة إلى class أو متغير عام. من أشهر استخداماتها: العدّادات، والدوال التي تعمل مرة واحدة فقط، ودوال الـ memoization، وإخفاء تفاصيل التنفيذ خلف واجهة صغيرة.
لماذا تتصرّف الـ Closures بشكل غريب داخل الحلقات مع var؟
لأن var نطاقها على مستوى الدالة بالكامل، فكل تكرار في الحلقة يتشارك نفس المتغير. كل الـ closures التي تُنشَأ داخل الحلقة تشير إلى هذا المتغير الواحد، وعند تنفيذها تجده قد وصل إلى قيمته النهائية. الحل هو استخدام let لأنها block-scoped، فيحصل كل تكرار على ربط (binding) مستقل.