إغلاقات JavaScript (Closures): دليلك الشامل لفهم سر الاحتفاظ بالقيم بين استدعاءات الدوال

دقائق القراءة: 11

مقدمة إلى إغلاقات JavaScript (Closures): لماذا هي ضرورية للمطورين؟

قد يبدو الفهم الكامل لمفهوم الإغلاقات (Closures) في JavaScript بمثابة اختبار حقيقي للمطورين. غالبًا ما يكون من الصعب استيعابها لأنها تُدرّس بطريقة قد لا تكون الأوضح، حيث يتم التركيز على تعريفها دون توضيح فائدتها العملية للمطور العادي أو كيفية تطبيقها في الكود الخاص به. لذا، لماذا تعتبر الإغلاقات مهمة في كود JavaScript اليومي؟

بدلاً من التعامل مع الإغلاقات كموضوع يجب حفظه لاختبار ما، دعنا نستكشف سلسلة من الخطوات التي ستقودنا إلى رؤية الإغلاق في المقام الأول. بمجرد أن ندرك ماهيتها، سنكتشف لماذا تستحق الإغلاقات معرفتك والاستفادة منها في كود JavaScript الخاص بك.

رؤية الإغلاق (Closure) في الممارسة العملية

لنفترض أننا نقوم بإنشاء نسخة مطابقة لتطبيق موقع المدونات Medium، ونريد أن يتمكن كل مستخدم من الإعجاب بمنشورات مختلفة. في كل مرة ينقر فيها المستخدم على زر الإعجاب، ستزداد قيمته بواحد. تخيل الأمر مثل زر التصفيق في Medium:

مثال توضيحي لزر الإعجاب أو التصفيق في تطبيق يشبه Medium، يوضح زيادة العدد عند النقر.

الدالة التي ستتولى زيادة العدد بواحد في كل مرة تُسمى handleLikePost، ونحن نتتبع عدد الإعجابات بمتغير يُسمى likeCount:

 // global scope
 let likeCount = 0;

 function handleLikePost() {
   // function scope
   likeCount = likeCount + 1;
 }

 handleLikePost();
 console.log("like count:", likeCount); // like count: 1

عندما يعجب المستخدم بمنشور، نستدعي الدالة handleLikePost()، وهي تزيد قيمة المتغير likeCount بواحد. وهذا يعمل لأننا نعلم أن الدوال يمكنها الوصول إلى المتغيرات خارج نطاقها. بعبارة أخرى، يمكن للدوال الوصول إلى أي متغيرات معرفة في أي نطاق أبوي.

مشكلة المتغيرات العامة (Global Variables)

يوجد مشكلة في الكود السابق. بما أن المتغير likeCount موجود في النطاق العام (global scope)، وليس داخل أي دالة، فإنه يعتبر متغيرًا عامًا. يمكن استخدام المتغيرات العامة (وتغييرها) بواسطة أي جزء آخر من الكود أو أي دالة في تطبيقنا. على سبيل المثال، ماذا لو قمنا عن طريق الخطأ بتعيين قيمة likeCount إلى صفر بعد دالتنا؟

 let likeCount = 0;

 function handleLikePost() {
   likeCount = likeCount + 1;
 }

 handleLikePost();
 likeCount = 0; // خطأ: إعادة تعيين قيمة المتغير العام
 console.log("like count:", likeCount); // like count: 0

بطبيعة الحال، لن يتمكن المتغير likeCount من الزيادة أبدًا من الصفر في هذه الحالة. عندما تحتاج دالة واحدة فقط إلى جزء معين من البيانات، يجب أن يكون هذا الجزء موجودًا محليًا، أي داخل تلك الدالة.

نقل المتغيرات إلى النطاق المحلي: تحديات جديدة

الآن دعنا ننقل المتغير likeCount داخل دالتنا:

 function handleLikePost() {
   // likeCount moved from global scope to function scope
   let likeCount = 0;
   likeCount = likeCount + 1;
 }

لاحظ أن هناك طريقة أقصر لكتابة السطر الذي نزيد فيه likeCount. بدلاً من القول إن likeCount يساوي القيمة السابقة لـ likeCount مضافًا إليها واحد، يمكننا ببساطة استخدام عامل التشغيل += كما يلي:

 function handleLikePost() {
   let likeCount = 0;
   likeCount += 1;
 }

ولكي يعمل الكود كما كان من قبل ويحصل على قيمة عدد الإعجابات، نحتاج أيضًا إلى نقل استدعاء console.log() إلى داخل الدالة:

 function handleLikePost() {
   let likeCount = 0;
   likeCount += 1;
   console.log("like count:", likeCount);
 }

 handleLikePost(); // like count: 1

لا يزال الكود يعمل بشكل صحيح كما كان من قبل. لذا، يجب أن يكون المستخدمون قادرين على الإعجاب بمنشور عدة مرات كما يريدون، فلنستدعي handleLikePost() بضع مرات أخرى:

 handleLikePost(); // like count: 1
 handleLikePost(); // like count: 1
 handleLikePost(); // like count: 1

عندما نقوم بتشغيل هذا الكود، هناك مشكلة. كنا نتوقع أن نرى المتغير likeCount يزداد باستمرار، لكننا نرى القيمة 1 في كل مرة. لماذا؟ خذ لحظة، انظر إلى الكود وحاول أن تشرح لماذا لم يعد المتغير likeCount يزداد. دعنا نلقي نظرة على دالة handleLikePost() وكيف تعمل:

 function handleLikePost() {
   let likeCount = 0;
   likeCount += 1;
   console.log("like count:", likeCount);
 }

في كل مرة نستخدم فيها الدالة، نقوم بإعادة إنشاء المتغير likeCount، الذي يتم إعطاؤه قيمة أولية 0. لا عجب أننا لا نستطيع تتبع العدد بين استدعاءات الدوال! يتم تعيينه إلى 0 في كل مرة، ثم يزداد بواحد، وبعد ذلك تنتهي الدالة من التشغيل. لذا نحن عالقون هنا. يجب أن يعيش متغيرنا داخل دالة handleLikePost()، ولكن لا يمكننا الحفاظ على العدد.

نحن بحاجة إلى شيء يسمح لنا بالحفاظ على قيمة likeCount أو تذكرها بين استدعاءات الدوال.

الحل: الدوال الداخلية (Inner Functions) وإعادة القيمة

ماذا لو جربنا شيئًا قد يبدو غريبًا بعض الشيء في البداية – ماذا لو حاولنا وضع دالة أخرى داخل دالتنا؟

 function handleLikePost() {
   let likeCount = 0;
   likeCount += 1;
   function() {
     // دالة داخلية فارغة حاليًا
   }
 }

 handleLikePost();

هنا سنسمي هذه الدالة addLike. السبب؟ لأنها ستكون مسؤولة عن زيادة المتغير likeCount الآن. ولاحظ أن هذه الدالة الداخلية لا يجب أن يكون لها اسم. يمكن أن تكون دالة مجهولة (anonymous function). في معظم الحالات، تكون كذلك. نحن فقط نعطيها اسمًا حتى نتمكن من التحدث عنها وعما تفعله بسهولة أكبر.

ستكون الدالة addLike الآن مسؤولة عن زيادة likeCount، لذا سننقل السطر الذي نزيد فيه بواحد إلى دالتنا الداخلية.

 function handleLikePost() {
   let likeCount = 0;
   function addLike() {
     likeCount += 1;
   }
 }

ماذا لو استدعينا دالة addLike() هذه داخل handleLikePost()؟ كل ما سيحدث هو أن addLike() ستزيد likeCount، ولكن لا يزال المتغير likeCount سيتم تدميره. لذا مرة أخرى، نفقد قيمتنا والنتيجة هي 0.

ولكن بدلاً من استدعاء addLike() داخل الدالة التي تحتويها، ماذا لو استدعيناها خارج الدالة؟ هذا يبدو أغرب. وكيف نفعل ذلك؟

نعلم في هذه المرحلة أن الدوال تُرجع قيمًا. على سبيل المثال، يمكننا إرجاع قيمة likeCount في نهاية handleLikePost() لتمريرها إلى أجزاء أخرى من برنامجنا:

 function handleLikePost() {
   let likeCount = 0;
   function addLike() {
     likeCount += 1;
   }
   addLike();
   return likeCount;
 }

ولكن بدلاً من القيام بذلك، دعنا نُرجع likeCount داخل addLike() ثم نُرجع دالة addLike() نفسها:

 function handleLikePost() {
   let likeCount = 0;
   return function addLike() {
     likeCount += 1;
     return likeCount;
   };
   // addLike(); // لا نستدعيها هنا
 }

 handleLikePost();

قد يبدو هذا غريبًا، لكنه مسموح به في JavaScript. يمكننا استخدام الدوال مثل أي قيمة أخرى في JS. هذا يعني أنه يمكن إرجاع دالة من دالة أخرى. من خلال إرجاع الدالة الداخلية، يمكننا استدعاؤها من خارج الدالة التي تحتويها. ولكن كيف نفعل ذلك؟ فكر في هذا لدقيقة وحاول أن تكتشف الأمر…

أولاً، لكي نرى ما يحدث بشكل أفضل، دعنا نستخدم console.log(handleLikePost()) عندما نستدعيها ونرى ما نحصل عليه:

 function handleLikePost() {
   let likeCount = 0;
   return function addLike() {
     likeCount += 1;
     return likeCount;
   };
 }

 console.log(handleLikePost()); // ƒ addLike()

كما هو متوقع، نحصل على الدالة addLike() مسجلة في الكونسول. لماذا؟ لأننا نُرجعها، بعد كل شيء. لاستدعائها، ألا يمكننا ببساطة وضعها في متغير آخر؟ كما قلنا للتو، يمكن استخدام الدوال مثل أي قيمة أخرى في JS. إذا كان بإمكاننا إرجاعها من دالة، يمكننا وضعها في متغير أيضًا. لذا دعنا نضعها في متغير جديد يُسمى like:

 function handleLikePost() {
   let likeCount = 0;
   return function addLike() {
     likeCount += 1;
     return likeCount;
   };
 }

 const like = handleLikePost();

وأخيرًا، دعنا نستدعي like(). سنفعل ذلك بضع مرات ونسجل كل نتيجة في console.log():

 function handleLikePost() {
   let likeCount = 0;
   return function addLike() {
     likeCount += 1;
     return likeCount;
   };
 }

 const like = handleLikePost();

 console.log(like()); // 1
 console.log(like()); // 2
 console.log(like()); // 3

لقد تم أخيرًا الحفاظ على قيمة likeCount! في كل مرة نستدعي فيها like()، تزداد قيمة likeCount من قيمتها السابقة. فما الذي حدث هنا بالضبط؟ حسنًا، لقد اكتشفنا كيفية استدعاء الدالة addLike() من خارج النطاق الذي تم تعريفها فيه. لقد فعلنا ذلك عن طريق إرجاع الدالة الداخلية من الدالة الخارجية وتخزين مرجع إليها، باسم like، لاستدعائها.

كيف تعمل الإغلاقات (Closures) خطوة بخطوة؟

كان ذلك هو تطبيقنا، بالطبع، ولكن كيف حافظنا على قيمة likeCount بين استدعاءات الدوال؟

 function handleLikePost() {
   let likeCount = 0;
   return function addLike() {
     likeCount += 1;
     return likeCount;
   };
 }

 const like = handleLikePost();
 console.log(like()); // 1

يتم تنفيذ الدالة الخارجية handleLikePost()، مما يؤدي إلى إنشاء نسخة من الدالة الداخلية addLike()؛ هذه الدالة (addLike) تُغلق على المتغير likeCount، الموجود في نطاق أعلى. لقد استدعينا الدالة addLike() من خارج النطاق الذي تم تعريفها فيه. لقد فعلنا ذلك عن طريق إرجاع الدالة الداخلية من الدالة الخارجية وتخزين مرجع إليها، باسم like، لاستدعائها.

عندما تنتهي الدالة like() من التشغيل، عادة ما نتوقع أن يتم جمع جميع متغيراتها كـ garbage collected (إزالتها من الذاكرة، وهي عملية تلقائية يقوم بها مترجم JS). كنا نتوقع أن تختفي كل قيمة likeCount عندما تنتهي الدالة، لكنها لا تختفي. ما هو السبب؟ الإغلاق (Closure). بما أن نسخ الدالة الداخلية لا تزال حية (مُعيّنة للمتغير like)، فإن الإغلاق لا يزال يحافظ على متغيرات likeCount.

قد تعتقد أن وجود دالة مكتوبة داخل دالة أخرى سيكون تمامًا مثل دالة مكتوبة في النطاق العام. لكن الأمر ليس كذلك. هذا هو السبب في أن الإغلاق يجعل الدوال قوية جدًا، لأنه خاصية فريدة لا توجد في أي شيء آخر في اللغة.

دورة حياة المتغيرات في JavaScript

لتقدير الإغلاقات بشكل أفضل، يجب أن نفهم كيف تتعامل JavaScript مع المتغيرات التي يتم إنشاؤها. ربما تساءلت عما يحدث للمتغيرات عندما تغلق صفحتك أو تنتقل إلى صفحة أخرى داخل تطبيق. ما هي مدة حياة المتغيرات؟

  • المتغيرات العامة (Global variables): تعيش حتى يتم التخلص من البرنامج، على سبيل المثال عند إغلاق النافذة. إنها موجودة طوال دورة حياة البرنامج.
  • المتغيرات المحلية (Local variables): لها حياة قصيرة. يتم إنشاؤها عند استدعاء الدالة، ويتم حذفها عند انتهاء الدالة.

لذا، في السابق، عندما كان likeCount مجرد متغير محلي، عند تشغيل الدالة، تم إنشاء المتغير likeCount في بداية الدالة ثم تم تدميره بمجرد الانتهاء من التنفيذ.

الإغلاقات ليست “لقطات ثابتة” للقيم

يُقال أحيانًا أن إغلاقات JavaScript تشبه اللقطات (snapshots)، أي صورة لبرنامجنا في نقطة زمنية معينة. هذا مفهوم خاطئ يمكننا تبديده بإضافة ميزة أخرى لوظيفة زر الإعجاب لدينا. لنفترض أنه في بعض المناسبات النادرة، نريد السماح للمستخدمين بـ “الإعجاب المزدوج” بمنشور وزيادة likeCount بمقدار 2 في كل مرة بدلاً من 1. كيف يمكننا إضافة هذه الميزة؟

طريقة أخرى لتمرير القيم إلى دالة هي بالطبع عبر الوسائط (arguments)، والتي تعمل تمامًا مثل المتغيرات المحلية. دعنا نمرر وسيطًا يُسمى step إلى الدالة، والذي سيسمح لنا بتوفير قيمة ديناميكية قابلة للتغيير لزيادة عددنا بدلاً من القيمة الثابتة 1.

 function handleLikePost(step) {
   let likeCount = 0;
   return function addLike() {
     likeCount += step; // الآن نستخدم step بدلاً من 1 الثابتة
     return likeCount;
   };
 }

بعد ذلك، دعنا نحاول إنشاء دالة خاصة تسمح لنا بالإعجاب المزدوج بمنشوراتنا، doubleLike. سنمرر 2 كقيمة لـ step لإنشائها ثم نحاول استدعاء كلتا دالتينا، like و doubleLike:

 function handleLikePost(step) {
   let likeCount = 0;
   return function addLike() {
     likeCount += step;
     return likeCount;
   };
 }

 const like = handleLikePost(1); // إغلاق لزيادة بمقدار 1
 const doubleLike = handleLikePost(2); // إغلاق لزيادة بمقدار 2

 console.log(like()); // 1
 console.log(like()); // 2
 console.log(doubleLike()); // 2 (العدد لا يزال محفوظًا بشكل مستقل!)
 console.log(doubleLike()); // 4

نرى أن likeCount يتم الحفاظ عليه أيضًا لـ doubleLike بشكل مستقل. ما الذي يحدث هنا؟ كل نسخة من الدالة الداخلية addLike() تُغلق على كل من المتغيرين likeCount و step من نطاق دالتها الخارجية handleLikePost(). تبقى قيمة step كما هي بمرور الوقت لكل إغلاق، ولكن العدد likeCount يتم تحديثه في كل استدعاء لتلك الدالة الداخلية.

بما أن الإغلاق يتم على المتغيرات وليس مجرد لقطات للقيم، فإن هذه التحديثات يتم الحفاظ عليها بين استدعاءات الدوال. فماذا يظهر لنا هذا الكود – حقيقة أنه يمكننا تمرير قيم ديناميكية لتغيير نتيجة دالتنا؟ إنها لا تزال حية! الإغلاقات تحافظ على المتغيرات المحلية حية من الدوال التي كان يجب أن تدمرها منذ فترة طويلة. بعبارة أخرى، إنها ليست ثابتة وغير متغيرة، مثل لقطة لقيمة المتغيرات المغلقة عليها في نقطة زمنية واحدة – الإغلاقات تحافظ على المتغيرات وتوفر رابطًا نشطًا لها. ونتيجة لذلك، يمكننا استخدام الإغلاقات لمراقبة أو إجراء تحديثات على هذه المتغيرات بمرور الوقت.

ما هو الإغلاق (Closure) بالضبط؟

الآن بعد أن رأيت كيف أن الإغلاق مفيد، هناك معياران لكي يكون الشيء إغلاقًا، وكلاهما رأيتهما هنا:

  • الإغلاقات هي خاصية لدوال JavaScript، والدوال فقط. لا يوجد نوع بيانات آخر يمتلكها.
  • لمراقبة إغلاق، يجب عليك تنفيذ دالة في نطاق مختلف عن المكان الذي تم تعريف تلك الدالة فيه في الأصل.

لماذا يجب أن تفهم الإغلاقات (Closures)؟

دعنا نجيب على السؤال الأصلي الذي طرحناه. بناءً على ما رأيناه، توقف وحاول الإجابة على هذا السؤال. لماذا يجب أن نهتم بالإغلاقات كمطوري JS؟

تعتبر الإغلاقات مهمة لك ولشفرتك لأنها تسمح لك بـ “تذكر” القيم، وهي ميزة قوية وفريدة جدًا في اللغة لا تمتلكها إلا الدوال. رأيناها هنا في هذا المثال. ففي النهاية، ما فائدة متغير عدد الإعجابات الذي لا يتذكر الإعجابات؟

ستواجه هذا غالبًا في مسيرتك المهنية في JS. ستحتاج إلى الاحتفاظ ببعض القيم بطريقة ما وربما إبقائها منفصلة عن القيم الأخرى. ماذا تستخدم؟ دالة. لماذا؟ لتتبع البيانات بمرور الوقت باستخدام إغلاق. وبهذا، تكون بالفعل متقدمًا بخطوة على المطورين الآخرين.

الخلاصة التقنية

الإغلاقات في JavaScript ليست مجرد مفهوم نظري معقد، بل هي أداة برمجية قوية وأساسية لحل مشاكل شائعة مثل الحفاظ على حالة المتغيرات بين استدعاءات الدوال وإدارة النطاقات بفعالية. إنها تمكننا من إنشاء دوال مصنعية (factory functions) تنتج دوال أخرى ذات حالة خاصة بها، مما يعزز مبدأ التغليف (encapsulation) ويقلل من الاعتماد على المتغيرات العامة الخطيرة. فهم الإغلاقات يفتح الباب أمام أنماط تصميم متقدمة ويجعل الكود أكثر مرونة وقابلية للصيانة، مما يرتقي بمهارات المطور إلى مستوى احترافي أعلى.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *