كيفية استخدام الإغلاقات في JavaScript: دليل مبسط للمبتدئين

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

مقدمة: لماذا تبدو الإغلاقات في JavaScript مربكة؟

يُعد مفهوم Closure من أكثر مفاهيم JavaScript التي يَصعُب فهمها في البداية، ليس لأنه معقد من الناحية النظرية فقط، بل لأن استخدامه يحدث غالباً بشكل غير مباشر. فأنت لا تقول عادة: «سأستخدم الآن Closure لحل هذه المشكلة»، ومع ذلك قد تكون استخدمته مرات كثيرة دون أن تنتبه.

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

شرح مفهوم الإغلاقات في جافاسكربت للمبتدئين مع أمثلة عملية

ما هو Closure في JavaScript؟

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

const value = 1

function doSomething() {
  let data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
  return data.filter(item => item % value === 0)
}

في المثال السابق، تستخدم الدالة doSomething المتغير value رغم أنه لم يُعرَّف داخلها. والأمر نفسه ينطبق على الدالة الداخلية:

function (item) {
  return item % value === 0
}

هذه الدالة تعتمد أيضاً على المتغير value المُعرّف خارج نطاقها المباشر، وهذا تحديداً هو جوهر Closure.

كيف تصل الدوال إلى متغيرات خارج نطاقها؟

في JavaScript يمكن للدوال الوصول إلى القيم الموجودة خارج جسمها طالما كانت ضمن سلسلة النطاقات المتاحة لها. انظر إلى المثال التالي:

let count = 1

function counter() {
  console.log(count)
}

counter() // print 1
count = 2
counter() // print 2

هنا تعتمد الدالة counter على المتغير count الموجود خارجها. وعندما تتغير قيمة count، فإن الدالة تستخدم القيمة الجديدة مباشرة عند الاستدعاء.

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

لماذا نستخدم الدوال أساساً؟

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

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

/* My wonderful piece of code */

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

أين يظهر Closure فعلياً؟

لنأخذ مثالاً بسيطاً ثم نغلفه داخل دالة أخرى:

function wonderfulFunction() {
  let count = 1

  function counter() {
    console.log(count)
  }

  counter() // print 1
}

في هذا المثال، لدينا الدالة counter التي تستخدم المتغير count، رغم أن count مُعرّف داخل الدالة الخارجية wonderfulFunction. هذا يعني أن counter أغلقت على ذلك المتغير، أي كوّنت Closure.

الفكرة الأساسية هنا هي:

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

ماذا يحدث بعد انتهاء تنفيذ الدالة الخارجية؟

عادةً، عندما تنتهي الدالة الخارجية من التنفيذ، تصبح متغيراتها الداخلية مرشحة للإزالة من الذاكرة بواسطة Garbage Collector. لكن إذا كانت هناك دالة داخلية ما زالت تحتاج إلى تلك المتغيرات، فإن المحرك يحتفظ بها.

يتضح ذلك أكثر في المثال التالي:

function wonderfulFunction() {
  let count = 1

  function counter() {
    count++
    console.log(count)
  }

  setInterval(counter, 2000)
}

wonderfulFunction()

هنا تُمرَّر الدالة counter إلى setInterval لكي تُنفَّذ كل ثانيتين. وبما أن counter ما تزال مستخدمة بعد انتهاء wonderfulFunction، فإن المتغير count يبقى موجوداً في الذاكرة أيضاً. هذا مثال عملي واضح على أثر Closure.

لماذا تدعم JavaScript هذا السلوك؟

السبب هو أن JavaScript تسمح بتعشيق الدوال، كما أن الدوال فيها تُعامل ككائنات من الدرجة الأولى First-Class Citizens. وهذا يعني أنه يمكن:

  • تعريف دالة داخل دالة أخرى.
  • تمرير الدوال كوسائط.
  • إرجاع الدوال من دوال أخرى.
  • تخزين الدوال داخل متغيرات أو خصائص.

كل هذه القدرات تجعل Closure أمراً طبيعياً ومتكرراً في كتابة الشيفرة الحديثة.

استخدامات عملية للإغلاقات في JavaScript

1) التعبير الدالي المنفذ فوراً IIFE

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

(function (arg1, arg2) {
  ...
  ...
})(arg1, arg2)

هذا الأسلوب يسمح بإنشاء متغيرات خاصة لا يمكن الوصول إليها من خارج الوحدة، وهو ما كان يُستخدم لمحاكاة الخصوصية Private Scope.

const module = (function () {
  function privateMethod() {}
  const privateValue = "something"

  return {
    get: privateValue,
    set: function (v) {
      privateValue = v
    }
  }
})()

var x = module()
x.get() // "something"
x.set("Another value")
x.get() // "Another Value"
x.privateValue // Error

الفكرة الجوهرية هنا أن القيمة privateValue لا يمكن الوصول إليها مباشرة من الخارج، لكن يمكن التعامل معها عبر الدوال التي أُرجعت من داخل النطاق نفسه.

2) مصنع الدوال Function Factory

من الأنماط المفيدة أيضاً نمط Function Factory، حيث تُنشئ دالةٌ ما دوالاً أخرى أو كائنات مخصصة. هذا النمط شائع عند بناء مكوّنات قابلة لإعادة الاستخدام أو نماذج بيانات مرنة.

const createUser = ({ userName, avatar }) => ({
  id: createID(),
  userName,
  avatar,
  changeUserName(userName) {
    this.userName = userName;
    return this;
  },
  changeAvatar(url) {
    // execute some logic to retrieve avatar image
    const newAvatar = fetchAvatarFromUrl(url)
    this.avatar = newAvatar
    return this
  }
});

console.log(createUser({ userName: 'Bender', avatar: 'bender.png' }));

يساعد هذا النمط على إنشاء كائنات متشابهة السلوك مع الاحتفاظ بمرونة عالية في التخصيص.

3) أسلوب Currying

Currying هو أسلوب برمجي تُقسَّم فيه الدالة التي تستقبل عدة معاملات إلى سلسلة من الدوال، تستقبل كل واحدة منها معاملاً واحداً ثم تعيد دالة أخرى.

هذا الأسلوب مفيد في:

  • التخصيص التدريجي للدوال.
  • إعادة استخدام أجزاء من المنطق.
  • بناء شيفرة قابلة للتركيب Composition.
function multiply(a) {
  return function (b) {
    return function (c) {
      return a * b * c
    }
  }
}

let mc1 = multiply(1);
let mc2 = mc1(2);
let res = mc2(3);
console.log(res);

let res2 = multiply(1)(2)(3);
console.log(res2);

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

ويمكن كتابة المثال نفسه بصيغة ES6 باستخدام Arrow Functions:

let multiply = (a) => (b) => (c) => {
  return a * b * c;
}

let mc1 = multiply(1);
let mc2 = mc1(2);
let res = mc2(3);
console.log(res);

let res2 = multiply(1)(2)(3);
console.log(res2);

أين يفيد Currying عملياً؟

من أشهر الاستخدامات العملية له بناء وظائف قابلة للتركيب، مثل إنشاء عناصر HTML أو تنسيق البيانات على مراحل.

function createElement(element) {
  const el = document.createElement(element)
  return function (content) {
    return el.textNode = content
  }
}

const bold = crearElement('b')
const italic = createElement('i')
const content = 'My content'
const myElement = bold(italic(content))
// <b><i>My content</i></b>

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

4) استخدام الإغلاقات مع معالجات الأحداث Event Listeners

من أكثر التطبيقات العملية للإغلاقات التعامل مع الأحداث، خصوصاً في واجهات المستخدم مثل React. لنفترض أن لديك مكوناً اسمه RenderItem ويقبل خاصية واحدة فقط هي onClick، وهذه الخاصية لا تستقبل أي معاملات.

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

// Closure
// with es5
function onItemClick(title) {
  return function () {
    alert("Clicked " + title)
  }
}

// with es6
const onItemClick = title => () => alert(`Clcked ${title} `)

return (
  <Container>
    {items.map(item => {
      return (
        <RenderItem onClick={onItemClick(item.title)}>
          <Title>{item.title}</Title>
        </RenderItem>
      )
    })}
  </Container>
)

في هذا المثال، تستقبل الدالة onItemClick قيمة title ثم تعيد دالة جديدة تطابق التوقيع المطلوب في onClick. وبفضل Closure تظل قيمة title متاحة عند تنفيذ الحدث لاحقاً.

أفضل الفوائد العملية لفهم Closures

  • كتابة شيفرة أكثر تنظيماً وقابلية لإعادة الاستخدام.
  • بناء دوال خاصة تحتفظ بحالتها الداخلية.
  • محاكاة الخصوصية في بعض الأنماط البرمجية.
  • تسهيل إنشاء Callbacks ومعالجات الأحداث.
  • فهم أفضل للأنماط الحديثة في JavaScript وأطر العمل.

أخطاء شائعة عند تعلم الإغلاقات

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

نصائح لفهم الإغلاقات بسهولة

  1. ابدأ بأمثلة صغيرة تحتوي على دالة داخل دالة.
  2. راقب المتغيرات المُعرّفة في النطاق الخارجي وكيف تُستخدم داخلياً.
  3. جرّب تغيير القيم قبل وبعد استدعاء الدوال.
  4. استخدم console.log() لتتبع القيم أثناء التنفيذ.
  5. طبّق المفهوم في أمثلة من الحياة العملية مثل الأزرار، المؤقتات، والمصانع البرمجية.

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

الإغلاقات في JavaScript ليست مجرد تفصيلة نظرية، بل هي آلية أساسية تؤثر في طريقة عمل الدوال، وإدارة الحالة، وبناء الأنماط البرمجية الحديثة. كلما فهمت العلاقة بين الدالة والنطاق والمتغيرات المحيطة بها، أصبحت أكثر قدرة على كتابة شيفرة نظيفة ومرنة وسهلة التوسعة. ومن الناحية العملية، فإن استيعاب Closures يفتح الباب أمام فهم أعمق لمفاهيم مثل Currying وIIFE وEvent Handlers داخل التطبيقات الحديثة.

ملخص بصري لمفهوم الإغلاقات في جافاسكربت وتطبيقاتها العملية

اترك تعليقاً

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