المستوى: مبتدئ — مش محتاج خبرة سابقة، بس تكون كتبت دالة (function) مرة قبل كده.
الإغلاقات (Closures): ليه الدالة تفضل فاكرة متغيّراتها بعد ما تخلص؟
لو كتبت دالة جوّه دالة تانية ولقيت الدالة الجوّانية لسه فاكرة متغيّر خلص عمره من زمان، ده مش عطل. ده الـ Closure. وهو الحاجة اللي بيقوم عليها نص كود جافاسكريبت الحديث: العدّادات، الـ private state، الـ event handlers، والـ debounce. تفهمه صح دلوقتي، هتبطّل تتلغبط في أخطاء غريبة بعد كده.
المشكلة باختصار
القاعدة اللي حافظها إن المتغيّر المحلي بيموت لما الدالة تخلص. طيب إزاي دالة جوّانية بترجع رقم بيزيد كل مرة، رغم إن الدالة الأم اشتغلت مرة واحدة وخلصت؟ ده اللي بيلخبط ناس كتير، وهو بالظبط شغل الـ Closure.
الفكرة بمثال قبل أي كود
خُد المثال ده الأول عشان تحس بالفكرة. موظف في مكتبة ماشي وهو شايل شنطة صغيرة، جوّاها ورقة مكتوب عليها رقم. كل ما حد ييجي يسأله، بيطلع الورقة، يزوّد الرقم واحد، ويرجّعها الشنطة. المكتبة ممكن تقفل، المدير ممكن يمشي، بس الموظف لسه ماسك شنطته والرقم اللي جوّاها ماحدش لمسه.
الدالة الجوّانية في جافاسكريبت بتعمل بالظبط كده. لما بتترجع من دالة أم، بتمشي وهي شايلة شنطة فيها المتغيّرات اللي اتولدت حواليها. الدالة الأم تخلص وتتمسح من الذاكرة، بس الشنطة تفضل مع الدالة الجوّانية طول ما هي عايشة.
التعريف العلمي الدقيق
بالتحديد: الـ Closure هو دالة متجمّعة مع البيئة المعجمية (lexical environment) اللي اتعرّفت جوّاها. النقطة المهمة إن الدالة بتمسك مرجع (reference) للـ scope بتاعها، مش نسخة من القيم. عشان كده بتشوف آخر قيمة للمتغيّر، ولو المتغيّر اتغيّر بعدين، الدالة بتشوف التغيير. الكلمة معجمي معناها إن الربط بيتحدّد من مكان كتابة الكود في الملف، مش من مكان استدعائه.
كود شغّال في 12 سطر
function makeCounter() {
let count = 0; // متغيّر محلي جوّه الدالة الأم
return function () { // الدالة الجوّانية = الـ closure
count += 1; // بتوصل لـ count رغم إن makeCounter خلصت
return count;
};
}
const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2
console.log(next()); // 3
ركز في اللي حصل: makeCounter اشتغلت مرة واحدة، رجّعت الدالة الجوّانية، وخلصت. المفروض count تتمسح دلوقتي. بس next بيفضل يزوّدها ويطبع 1 ثم 2 ثم 3. ده لإن next ماسك مرجع للـ scope اللي فيه count. كل نداء لـ makeCounter() بيعمل شنطة جديدة مستقلة تمامًا.
الفخّ الشائع اللي بيوقّع المبتدئين
المثال ده بيطلع نتيجة غلط بيتفاجئ بيها الناس. الافتراض إنه هيطبع 0 و1 و2:
var handlers = [];
for (var i = 0; i < 3; i++) {
handlers.push(function () { return i; });
}
console.log(handlers.map(h => h())); // [3, 3, 3] مش [0, 1, 2]
ليه؟ لإن var بتعمل binding واحد للـ i على مستوى الدالة كلها. الدوال التلاتة كلها بتشير لنفس الـ i. ولما الحلقة تخلص، i بقت 3، فكلهم بيقروا 3. الحل إنك تدي كل closure نسخته الخاصة، وده اللي let بتعمله:
const handlers = [];
for (let i = 0; i < 3; i++) {
handlers.push(function () { return i; });
}
console.log(handlers.map(h => h())); // [0, 1, 2]
let بتعمل binding جديد كل لفة، فكل closure بيمسك شنطته لوحده. ده مش تفصيلة صغيرة، ده سبب حقيقي لباجات في الـ event listeners جوّه الحلقات.
الـ trade-offs وحاجة لازم تنتبه لها
الـ Closure بيكسّبك حاجة قوية: حالة خاصة (private state) من غير كلاسات، وذاكرة بتعيش بين النداءات. بس بتدفع تمنها. الـ trade-off هنا في الذاكرة: الـ closure بيمسك الـ scope بتاعه حيّ طول ما هو موجود. لو عندك 10,000 closure، وكل واحد ماسك من غير داعي مصفوفة حجمها 1 ميجابايت، دي 10 جيجابايت رام مش بتتحرّر. الـ Garbage Collector مش هيقدر يمسحها لإن لسه فيه مرجع شايلها. الافتراض إن الكلام ده على جافاسكريبت الحديثة (ES6 فأحدث) في المتصفح أو Node.js.
متى لا تستخدم هذه الطريقة
لو محتاج القيمة مرة واحدة وخلاص، متلفّهاش في closure؛ استخدم متغيّر عادي وريّح نفسك. ولو بتعمل closures جوّه حلقة على بيانات ضخمة وبتخزّنها في مصفوفة تعيش طويل، راقب الذاكرة بـ DevTools، لإن ده أكتر مكان بيتسرّب منه الرام من غير ما تحس. الـ Closure أداة، مش زينة تحطها في كل مكان.
الخطوة التالية
افتح الكونسول في المتصفح دلوقتي، انسخ دالة makeCounter اللي فوق وجرّبها. بعدين في مثال الحلقة، غيّر let لـ var وشوف بعينك الفرق بين 0 و1 و2 وبين 3 و3 و3. لو طلعلك نفس الناتج في الحالتين، يبقى في إعداد في الـ transpiler عندك (زي Babel) بيحوّل let لـ var، وده معناه إنك محتاج تراجع الـ target بتاع البناء.
المصادر
- MDN Web Docs — Closures: developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
- MDN — let statement: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
- ECMAScript Language Specification: tc39.es/ecma262
- Kyle Simpson — You Don't Know JS: Scope and Closures: github.com/getify/You-Dont-Know-JS