في تدوينة الأسبوع الماضي تكلمنا عن مفهوم ال Scope في JavaScript وقلنا بأنه واحد من المفاهيم الأساسية التي يجب على كل مطور جافا سكريبت أن يفهمه جيدا.
في درسنا هذا سنتكلم عن مفهوم وميزة أخرى من مزايا جافا سكريبت الأساسية، ميزة ال Closures التي يمكن اعتبارها الأهم على الإطلاق في JavaScript، خاصة إذا علمنا أنها الأرضية التي بنيت عليها العديد من المفاهيم الأساسية الأخرى في هذه اللغة مثل ال Callbacks وال Events.
مفهوم ال Closure يجده كثيرون صعب الإستيعاب والفهم، لذلك سنعمل في هذه التدوينة على شرحه وتشريحه بشكل مفصل وبسيط. وأعدكم أنه في الدرس المقبل سنحاول تطبيق ما سنتعلمه اليوم في بناء مثال عملي يبين قوة ودور ال Closures في تطبيقات جافا سكريبت.
دعونا اليوم نركز على شرح الأساسيات أولا وترسيخها في أذهاننا.
توطئة
تكلمنا في مقالنا السابق عن Lexical Scope وقدرة الدوال في جافا سكريبت على الوصول إلى المتغيرات المنتمية إلى النطاقات المحيطة بها.
كل دالة تستخدم متغيرات مصرح بها خارجها يمكن اعتبارها Closure.
دعونا نأخذ هذا المثال التالي ونقوم بدراسته:
function createCounter() {
let counter = 0;
function incrementCounter() {
counter = counter + 1;
console.log(counter);
}
return incrementCounter;
}
لدينا دالة اسمها createCounter
وبداخلها متغيرين اثنين:
- المتغير
counter
وقيمته الأولية تساوي0
. - المتغير
incrementCounter
وهو عبارة عن دالة تقوم بزيادة قيمةcount
(الموجود في lexical scope) بمقدار1
. - بعد زيادة العداد نقوم بطباعة قيمته الجديدة بواسطة
console.log
.
الدالة createCounter
تقوم في النهاية بإرجاع الدالة incrementCounter
لكي نقوم باستخدامها في وقت لاحق.
هذا يعني أن createCounter
لا تقوم بتغيير قيمة count
، بل دورها يكمن فقط في إنشاء ذلك المتغير وإعطائه قيمة بدئية وإرجاع الدالة incrementCounter
تتولى مهمة زيادة قيمة العداد.
بعد ذلك تنسحب createCounter
كليا من المشهد، فيتم تنقية الذاكرة من كل ما يخص هذه الدالة بما في ذلك المتغيرات المصرح بها بداخلها.
دعونا إذن نقوم بزيادة قيمة العداد count
بواسطة الدالة المرجعة من createCounter
:
const increment = createCounter();
increment(); // 1
increment(); // 2
إذا نفذنا الكود فإننا سنرى أن قيمة count
تساوي 1
بعد تنفيذ increment
للمرة الأولى، وستصبح تلك القيمة 2
بعد استدعائها مرة ثانية.
ولكن ألم نقل من قبل أنه فور تنفيذ دالة في جافاسكريبت يتم التخلص من كل ما له علاقة بها ؟ كل المتغيرات بداخلها. إذن من المفروض أنه في السطر الثاني من الكود أعلاه لن يكون هناك وجود للمتغير count
حيث أن createCounter
تم تنفيذها في السطر الأول وانتهى دورها!
نعم هذا ما قلناه في البداية وهو صحيح! إلا أن هناك ميزة في جافا سكريبت اسمها ال Closures تستطيع الإلتفاف على تلك الحقيقة.
دعوني أشرح لكم كيف بقي المتغير count
حيا وموجودا في برنامجنا رغم أن الدالة createCounter
التي ينتمي لنطاقها قد انتهى دورها وصارت ذكرى من الماضي.
ذكريات من الماضي
عندما قمنا بتنفيذ الدالة createCounter
قامت بإرجاع دالة أسندنا قيمتها للمتغير increment
.
أثناء هذه العملية وقبل أن تودع الدالة المرجعة (التي كان اسمها incrementCounter
) أمها المسماة createCounter
، قامت بجمع كل تلك المتغيرات التي تجمعها بها ووضعتها في محفظتها لكي تبقى ذكريات من الماضي تذكرها بأمها الغالية التي ستموت بعد لحظات.
المتغير count
كان في حالة مثالنا واحدا من تلك الأغراض التي بقيت لدالتنا الوليدة من أمها التي ماتت ولقيت مصرعها أثناء عملية الولادة.
لقد أصبح بإمكان الدالة الوليدة الآن increment
فتح محفظتها والوصول إلى ذلك المتغير لتشم رائحته التي تذكرها بأمهما، وتزيد قيمته بمقدار 1
في كل مرة تتذكره فيها 😢
في كل مرة تتذكر increment
أخاها العداد count
تفتح المحفظة التي في ظهرها، تزيد قيمته بمقدار 1
ثم ترجعه لمكانه في المحفظة. لذلك لاحظنا أن القيمة الأخيرة للعداد تظل محفوظة في انتظار الإستعمالات القادمة.
مثال آخر
دعونا نأخذ مثالا آخر، لنرسخ كل ما قلناه أعلاه في عقولنا.
function incrementAfterThreeSeconds() {
let count = 1;
setTimeout(function () {
count++;
console.log("count = ", count);
}, 3000);
}
incrementAfterThreeSeconds();
المثال واضح وبسيط جدا.
لدينا دالة اسمها incrementAfterThreeSeconds
تضم متغيرا اسمه count
. طلبنا من هذا المتغير أن تتغير قيمته من 1
إلى 2
بعد ثلاث ثواني.
الدالة incrementAfterThreeSeconds
نفذت في الأخير، وربما لا تعلمون بأن محرك الجافاسكريبت JavaScript Engine سيتخلص منها وكل ما يتعلق بها فور أن تنفذ بدون أن ينتظر انتهاء مدة الثلاث ثواني التي طلبتها setTimeout
! فكيف إذن سنتمكن من الوصول إلى count
بعد انقضاء تلك المدة ؟
الجواب دائما هو: شكرا لِ JavaScript Closures، وليدم الحب الأبدي الذي يجمع الدوال الوليدة بإخوانها.
قبل أن تلقى الأم المسكينة incrementAfterThreeSeconds
مصيرها المحتوم، طلبت من Event Loop عن طريق setTimeout
أن يقوم بتنفيذ الدالة المجهولة Anonymous Function (أول بارامتر ل setTimeout
) بعد مرور 3 ثواني.
هذه الدالة المجهولة الوليدة قبل أن يُلقى بها في Event Loop أطلقت صرختها الأولى في هذه الحياة وكأنها علمت بموت أمها، وجعلتها فطرتها وغريزتها أن تأخذ معها كل ما يجمعها بأمها المرحومة، وهو في هذه الحالة أخوها count
الذي وضعته في محفظتها المعروفة وتعود إليه كلما أرادت أن تشم فيه رائحة أمهما.
- راجع موضوعنا عن Event Loop
في مثالنا هذا، الأخت Anonymous Function ستفتح المحفظة بعد ثلاث ثواني لتزيد قيمة أخيها بمقدار 1
وتستعرض قيمته عن طريق console.log ثم تختفي من الوجود 😢
ال Closures في كل مكان
إذا نظرت إلى الجافا سكريبت فستجد ال Closures مع محافظها في كل مكان! في الأحداث Events وال Callbacks وغيرها!
كل مطور جافا سكريبت، حتى إن كان مبتدئا، سبق له استعمال ال Closures مرات كثيرة سواء علم بذلك أم لم يعلم.
إذا كنت مطور React.js وأنت تكتب هذا الكود:
const [count, setCount] = React.useState(0);
فاعلم أنك فعليا تستخدم ال Closures ولو من دون علم أو قصد. لولا مفهوم ال Closure لما وجدت أصلا ميزة الخُطافات أو Hooks في مكتبة React.
إذا لم تصدقني فانتظرني الأسبوع المقبل! سنرى حينها معا في درس جديد كيف تعمل خُطافات React.js خلف الكواليس وكيف تستعين بقوة ال Closures لتنجز عملها الرائع.
في انتظار ذلك، دعونا نحيي كل Closure نصادفها في أكوادنا ونخلذ ذكرى ذلك الحب العظيم الذي تكنه لإخوتها وأمهاتها. 💓