شرح ال Closures في جافا سكريبت.. ذكريات من الماضي

في تدوينة الأسبوع الماضي تكلمنا عن مفهوم ال 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 الذي وضعته في محفظتها المعروفة وتعود إليه كلما أرادت أن تشم فيه رائحة أمهما.

في مثالنا هذا، الأخت 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 نصادفها في أكوادنا ونخلذ ذكرى ذلك الحب العظيم الذي تكنه لإخوتها وأمهاتها. 💓

يمكنك دعمنا عبر منصة باتريون لكي نستطيع تسخير وقت أكبر في نشر وتجديد المحتوى.
الدعم سيساعدنا كذلك على تطوير ذلك المحتوى وتحسينه.

ادعمنا على باتريون