دليلك المبسط إلى فهم Closures في JavaScript

وليد الفيفيمنذ أسبوع

من أحد أهم المواضيع المهمة وصعبة الفهم في نفس الوقت في JavaScript هو ما يسمى Closure.

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

ما هو Closure؟

بعيداً عن أي تعريف أكاديمي، Closure هو ببساطة دالة (function) تحتفظ بالحالة (state) المحيطة بها والتي تسمى (lexical environement). هذه الحالة تعبر عن المتغيرات الموجودة ضمن نطاق (scope) الدالة الخارجية (outer function) التي تحتوي هذه الدالة الداخلية (inner function).

لتبسيط الأمور، بإمكانك النظر إلى هذا الرسم الذي يوضح معنى Closure بشكل نظري:

دعنا نحلل الصورة بالتفصيل.

- أولاً الدالة الخارجية تعرف متغيراً يسمى a وتسند له قيمة 5.

- الدالة الخارجية لديها دالة داخلية معرفة بداخلها.

- الدالة الداخلية تعرف متغيراً اسمه b وتسند له قيمة 10.

لا شيء جديد حتى الآن.

- الآن الدالة الداخلية تحاول طباعة المتغيرين a و b عن طريق العبارة console.log.

السؤال يأتي الآن، كيف للدالة الداخلية أن تعرف عن المتغير a الذي لم تقم بتعريفه بنفسها؟ هذا هو بالضبط ما يسمى Closure.

إذاً، مرة أخرى. Closure هو دالة تحتفظ بالحالة المحيطة بها (متغيرات) والتي تكون موجودة ضمن نطاق الدالة الخارجية.

مثال بسيط على Closure 

الآن بعد أن أخذنا فكرة عامة عن معنى Closure، دعنا نحاول فهمه بمثال عملي.

1 function outer() {
2   var a = 5;
3 
4   function inner() {
5     var b = 10;
6     alert(a + b);
7   }
8 
9   return inner;
10 }

لدينا هنا دالتين:

- الدالة الأولى outer والتي تعرف متغير a و تعيد الدالة الداخلية inner.

- الدالة الثانية inner والتي تعرف متغير b و تعرض تنبيهاً بقيمة a + b.

جميل. الآن دعنا نضف سطراً آخر كالتالي:

var inner = outer();

وأخيراً جرب أن تستدعي الدالة inner كالتالي:

inner();

ستلاحظ أن القيمة 15 (5 + 10) تنعرض على الشاشة بنجاح. ماذا يعني هذا؟ دعنا نحلل ما حدث عندما قمنا باستدعاء الدالة outer:

  1. تم إنشاء متغير باسم a يحمل قيمة 5 داخل الدالة outer. هذا المتغير يقع ضمن نطاق (scope) الدالة outer.
  2. الدالة outer تعيد الدالة المعرفة باسم inner.
  3. انتهى تنفيذ الدالة outer. تم التخلص من المتغير a الآن بما أن الدالة التي يقع ضمن نطاقها قد أنهت عملها.
  4. تم إسناد القيمة المرجعة من outer إلى متغير باسم inner.

جميل، الآن دعنا نحلل ما حدث عندما قمنا باستدعاء الدالة inner:

  1. تم إنشاء متغير باسم b يحمل قيمة 10 داخل الدالة inner. هذا المتغير يقع ضمن نطاق (scope) الدالة inner.
  2. الدالة inner تحاول عرض تنبيه alert على الشاشة يحمل قيمة a + b.
  3. الدالة inner تعرف جيداً المتغير b فقد قامت بتعريفه للتو. ولكن ماذا عن المتغير a؟ لاحظ أنه قد تم التخلص من هذا المتغير بالفعل عندما أنهت الدالة الخارجية outer عملها.

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

هنا يأتي دور Closure في JavaScript.

ما حدث فعلياً، هو أنه عند تعريفها في السطر 4 قامت الدالة الداخلية inner بعمل Closure أو (close over) أو إغلاق على المتغيرات التي تحتاجها في جسم الدالة (function body) والتي هي في حالتنا هنا المتغير a.

بهذه الطريقة يمكن للدالة الداخلية inner أن تتعرف على المتغير a حتى عندما يكون هذا المتغير قد تم التخلص منه بالفعل مع انتهاء تنفيذ الدالة الخارجية outer. 

هذا ما يسمى Closure في JavaScript.

ولرؤية الأمر بشكل بصري، جرب كتابة الأمر التالي وقم بتشغيل الكود مرة أخرى:

console.dir(inner);

سوف تظهر لك هذه الصورة في المتصفح:

لاحظ في السطر الثالث من الأخير أن هناك Closure يحمل قيمة المتغير a والتي تساوي 5.

لاحظ أيضاً أن هذا الClosure موجود تحت مصفوفة اسمها Scopes وهذا يعيدنا لتعريف Closure في البداية حيث ذكرنا أن Closure يسمح للدالة بالوصول إلى متغيرات موجودة في نطاق الدالة (scope) الخارجية.

تستطيع الوصول للكود كاملاً على هذا الرابط: Closures-1

Closure في مثال عملي أكثر

بعد أن فهمنا ما هو Closure بشكل أفضل، دعنا الآن ننظر إلى مثال عملي أكثر.

في الويب، الكثير من العمليات التي تحدث تكون event-based أي تحدث كردة فعل لحدث معين مثل النقر على زر، تحريك الفأرة وغيرها.

في هذا المثال، سنقوم بعرض بيانات المستخدم بناء على الاسم المختار من القائمة. هذا الاستخدام شائع جداً عند محاولة عرض بيانات بشكل ديناميكي (dynamic) والتي قد تستقبلها من API خارجي مثلاً.

الكود موجود على هذا الرابط: Closures-2

سيتضح كل شيء مع المثال، لذا دعنا نبدأ:

أولاً، لنضع الأساسات الضرورية من HTML و CSS:

HTML

<ul id="users_ul"></ul>
<div>
  <p>
    الاسم الأول: <span id="first_name"></span>
  </p>
  <p>
    الاسم الأخير: <span id="last_name"></span>
  </p>
  <p>
    العمر: <span id="age"></span>
  </p>
</div>

CSS

body {
  text-align: center;
  font-size: 24px;
  direction: rtl;
}

ul {
  list-style: none;
  padding: 15px;
  background-color: #777;
  width: 50%;
  margin: 0 auto;
}

li {
  padding: 10px;
  margin: 5px 0;
  cursor: pointer;
  background-color: #f3f3f3;
  font-size: 24px;
}

الآن لنبدأ بالجزء الخاص بJavaScript وهو ما يهمنا في هذا الدرس.

بداية سنقوم بجلب جميع عناصر HTML التي سنحتاج إلى التعامل معها كالتالي:

// DOM queries
const ul = document.getElementById("users_ul");
const first_name_el = document.getElementById("first_name");
const last_name_el = document.getElementById("last_name");
const age_el = document.getElementById("age");

والآن دعنا نعرف مصفوفة تحتوي على بيانات تجريبية للمستخدمين.

// Users data
const users = [
  {
    first_name: "أحمد",
    last_name: "علي",
    age: 25
  },
  {
    first_name: "خالد",
    last_name: "عيسى",
    age: 31
  },
  {
    first_name: "محمد",
    last_name: "أنس",
    age: 29
  }
];

لاحظ كيف أن هذه البيانات تشبه كثيراً البيانات التي قد تحصل عليها على هيئة JSON.

والآن لنأتي للجزء الذي يتعلق بالتعامل مع Closures:

بدايةً سنقوم بتعريف دالة اسمها renderUsers. داخل جسم هذه الدالة سنقوم بالتالي:

- سنقوم بالمرور على جميع عناصر المصفوفة users عن طريق التابع forEach.

- لكل عنصر من عناصر المصفوفة (أي لكل مستخدم موجود لدينا) سنقوم بإنشاء عنصر li ونعطيه قيمة نصية بالاسم الأول للمستخدم.

- سوف نضيف حدث نقر (click) لهذا العنصر li.

- عند تنفيذ هذا الحدث (النقر) سوف يتم تحديث الجزء الخاص بعرض البيانات ليعكس بيانات هذا المستخدم.

- أخيراً سنرفق هذا العنصر li إلى القائمة الغير مرتبة ul التي أنشئناها في البداية.

بعدها سنقوم فقط باستدعاء الدالة renderUsers ليتم تنفيذ الأوامر في الأعلى.

هذا هو الكود:

function renderUsers() {
  users.forEach((user) => {
    const user_li = document.createElement("li");

    user_li.innerText = user.first_name;

    user_li.addEventListener("click", function () {
      first_name_el.innerText = user.first_name;
      last_name_el.innerText = user.last_name;
      age_el.innerText = user.age;
    });

    ul.appendChild(user_li);
  });
}

renderUsers();

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

حسناً، من الواضح أن قيمة المستخدم user تتغير مع كل دورة (iteration) للتابع forEach. في نفس الوقت، نحن نستخدم هذه القيمة في الدالة الخاصة بالتعامل مع حدث النقر على عنصر user_li.

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

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

أرجو أن تكون قد عرفت السبب وراء هذا السلوك بالفعل. نعم، إنه Closure مرة أخرى.

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

الآن دعنا نرى القيم التي يحتفظ بها Closure عند النقر، كما فعلنا بالأمر console.dir ولكن هذه المرة بطريقة أخرى.

في المتصفح، قم بفتح نافذة فحص (inspect) وعن طريق التبويبة Elements اذهب للعنصر li الذي يحمل الاسم أحمد. ستجد على الجانب نافذة بها عدة تبويبات، افتح منها تبويبة Event Listeners واذهب لحدث النقر click.

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

تعقيب سريع

حاولت أن أشرح الفكرة الأساسية وراء Closures ولكني أترك لك مهمة التعمق في التطبيقات العملية لClosures. في الحقيقة، Closures من المواضيع المهمة جداً في JavaScript وفهمك لها سيساعدك كثيراً عند تتبع الأخطاء (debugging).

تركت لك في الأسفل مجموعة مصادر مفيدة (بالإنجليزية) يمكن أن تطلع عليها للاستفادة أكثر.

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

 

مصادر:

Closures - JavaScript | MDN

A simple guide to help you understand closures in JavaScript

كلمات دليلية: closures javascript
1
إعجاب
89
مشاهدات
0
مشاركة
2
متابع

التعليقات (0)

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !