شرح JavaScript Closures والنطاق ببساطة

Frontend2026-09-19TryQuickToolBox

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

ما هو النطاق في JavaScript؟

يحدد النطاق مكان إمكانية الوصول إلى المتغيرات في الكود الخاص بك. يحتوي JavaScript على ثلاثة أنواع رئيسية من النطاق:

إليك مثال سريع:

let globalVar = 'I am global';

function myFunc() {
  let functionVar = 'I am in function scope';
  if (true) {
    let blockVar = 'I am in block scope';
    console.log(globalVar); // accessible
    console.log(functionVar); // accessible
    console.log(blockVar); // accessible
  }
  console.log(blockVar); // ReferenceError: blockVar is not defined
}

myFunc();
console.log(functionVar); // ReferenceError: functionVar is not defined

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

Lexical Scoping: أساس Closures

يستخدم JavaScript lexical scoping، مما يعني أن نطاق المتغير يتم تحديده من خلال موقعه في الكود المصدري. يمكن للدوال الداخلية الوصول إلى المتغيرات في نطاقها الخارجي (المحيط). يُسمى هذا أيضًا static scoping.

تأمل هذا:

function outer() {
  let outerVar = 'outer';
  function inner() {
    console.log(outerVar); // 'outer'
  }
  inner();
}
outer();

يمكن لدالة inner الوصول إلى outerVar لأنها موجودة معجميًا داخل outer. هذا ليس ديناميكيًا؛ لا يعتمد على كيفية استدعاء الدالة.

ما هو Closure؟

Closure هي دالة تتذكر نطاقها المعجمي حتى عندما يتم تنفيذ الدالة خارج ذلك النطاق. بعبارة أخرى، يمنحك closure إمكانية الوصول إلى نطاق دالة خارجية من دالة داخلية. يتم إنشاء closures في كل مرة يتم فيها إنشاء دالة، في وقت إنشاء الدالة.

مثال:

function makeCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2

هنا، تُرجع makeCounter دالة داخلية تزيد count. حتى بعد انتهاء تنفيذ makeCounter، لا تزال الدالة المُرجعة قادرة على الوصول إلى count. هذا هو closure.

كيف تعمل Closures تحت الغطاء

عند إنشاء دالة، فإنها تخزن مرجعًا إلى بيئتها المعجمية (سلسلة النطاق). عند استدعاء الدالة، تستخدم تلك البيئة لحل المتغيرات. تبقى البيئة حية طالما أن closure موجود.

الاستخدامات العملية لـ Closures

Closures ليست مجرد نظرية؛ إنها مستخدمة في كل مكان في JavaScript.

1. خصوصية البيانات (Module Pattern)

قبل وحدات ES6، كانت closures تُستخدم لإنشاء متغيرات خاصة:

const counterModule = (function() {
  let count = 0;
  return {
    increment: function() { count++; },
    getCount: function() { return count; }
  };
})();

counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined

هنا، count خاص ويمكن الوصول إليه فقط عبر الطرق المُرجعة.

2. معالجات الأحداث وعمليات الاستدعاء

غالبًا ما تُستخدم closures في معالجات الأحداث للوصول إلى المتغيرات من النطاق الخارجي:

function setupButton() {
  let clickCount = 0;
  document.getElementById('myButton').addEventListener('click', function() {
    clickCount++;
    console.log(`Button clicked ${clickCount} times`);
  });
}

يتذكر closure معالج الحدث clickCount.

3. مصانع الدوال

يمكنك إنشاء دوال بوسائط محددة مسبقًا:

function multiplyBy(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = multiplyBy(2);
console.log(double(5)); // 10

المزالق الشائعة وكيفية تجنبها

يمكن أن تؤدي closures إلى سلوك غير متوقع إذا لم يتم فهمها.

التقاط متغير الحلقة

استخدام var في الحلقات ينشئ ارتباطًا مشتركًا واحدًا:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // prints 3, 3, 3
  }, 100);
}

جميع عمليات الاستدعاء تشترك في نفس i. لإصلاح ذلك، استخدم let (نطاق الكتلة) أو IIFE:

for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // prints 0, 1, 2
  }, 100);
}

تسريبات الذاكرة

تحافظ closures على المتغيرات حية. إذا تم ربط closure بكائن طويل العمر (مثل عنصر DOM)، فإنه يمكن أن يمنع جمع القمامة. قم دائمًا بإزالة مستمعي الأحداث عندما لا تكون هناك حاجة إليها.

سلسلة النطاق وحل المتغيرات

عند الوصول إلى متغير، يبحث JavaScript في سلسلة النطاق: أولاً النطاق الحالي، ثم النطاقات الخارجية، وصولاً إلى النطاق العام. إذا لم يتم العثور عليه، يتم رفع ReferenceError.

مثال:

let a = 'global';
function outer() {
  let b = 'outer';
  function inner() {
    let c = 'inner';
    console.log(a, b, c); // all accessible
  }
  inner();
}
outer();

سلسلة النطاق لـ inner هي: inner -> outer -> global.

المقارنة: var و let و const

الكلمة المفتاحيةالنطاقالرفع (Hoisting)قابل لإعادة التعيين
varالدالةنعم (يتم تهيئته إلى undefined)نعم
letالكتلةنعم (لكن غير مهيأ، TDZ)نعم
constالكتلةنعم (لكن غير مهيأ، TDZ)لا

فهم هذه الاختلافات هو المفتاح لتجنب الأخطاء المتعلقة بالنطاق.

الأسئلة الشائعة

ما هو closure في JavaScript؟

Closure هي دالة تحتفظ بالوصول إلى نطاقها المعجمي حتى عند تنفيذها خارج ذلك النطاق. تسمح للدالة "بتذكر" المتغيرات من المكان الذي تم تعريفها فيه.

لماذا تهم Closures؟

تمكّن closures خصوصية البيانات ومصانع الدوال ومعالجات الأحداث التي تحافظ على الحالة. إنها أساسية للعديد من أنماط JavaScript.

كيف يمكنني تجنب الأخطاء المتعلقة بـ closure؟

استخدم let و const لنطاق الكتلة، وكن حذرًا من الحلقات، ونظف مستمعي الأحداث لمنع تسريبات الذاكرة.

هل أنت مستعد لاختبار فهمك؟ جرب JSON Formatter الخاص بنا لتجربة مقتطفات الكود ورؤية كيفية عمل closures في الوقت الفعلي.