أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Closures في JavaScript للمبتدئ: ليه الدالة الجوّانية بتفتكر متغيّرات خرجت من النطاق

📅 ١ مايو ٢٠٢٦⏱ 6 دقائق قراءة
Closures في JavaScript للمبتدئ: ليه الدالة الجوّانية بتفتكر متغيّرات خرجت من النطاق

المستوى المطلوب: مبتدئ. لو شغّلت JavaScript قبل كده وفهمت معنى الدالة والمتغيّر، الكلام ده ليك. مش مطلوب منك تعرف لا async ولا prototypes ولا حتى ES Modules.

هتفهم في المقال ده ليه الدالة الجوّانية بتفتكر متغيّرات الدالة الخارجية حتى بعد ما الخارجية خلصت شغلها. ده مش تفصيلة أكاديمية — ده اللي بيخلّي React hooks تشتغل، وبيخلّي مكتبات زي Lodash تقدر تعمل debounce و throttle، وبيخلّيك تكتب counter آمن من غير ما حد يعدّل قيمته من بره.

Closures في JavaScript: المتغيّرات اللي مش بتموت لمّا الدالة تخلص

المشكلة باختصار

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

الكود ده مثلاً، لو سألتك هيطبع كام، إيه إجابتك؟

JavaScript
function makeGreeter(name) {
  return function () {
    console.log("أهلاً يا " + name);
  };
}

const greet = makeGreeter("حايس");
greet();

الإجابة الحدسية: undefined أو خطأ، لأن name اتعرّف داخل makeGreeter اللي خلصت تنفيذها بالفعل. الإجابة الفعلية: "أهلاً يا حايس". ده Closure.

محرر كود JavaScript على شاشة داكنة يعرض دالة داخل دالة لتوضيح فكرة الـ Closure

المثال أولاً: درج المكتب

تخيّل إن عندك مكتب فيه 5 أدراج. كل درج جوّاه ورقة مكتوب عليها رقم. الأدراج كلها تابعة للمكتب — لو الشركة قرّرت تشيل المكتب، الأدراج بتتشال معاه طبيعي.

دلوقتي تخيّل سيناريو غريب: ضفت لمكتبك ساعة حائط معلّقة فوقه، والساعة دي بتقرأ الرقم اللي في الدرج الأوّل وتظهره. لمّا الشركة شالت المكتب، الساعة فضلت معلّقة على الحيط، ولمّا حد سألها "إيه الرقم؟" قالتله الرقم اللي كان في الدرج الأول. ليه؟ لأن الساعة "فاكرة" مكان الدرج، ومادام في حد فاكر، الدرج عمره ما هيتشال فعلياً من المخزن.

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

التعريف العلمي الدقيق

الـ Closure هو دالة مرتبطة بـ الـ lexical environment اللي اتعرّفت فيه. الـ lexical environment ده هيكل بيانات داخلي في محرّك JavaScript بيتخزّن فيه: المتغيّرات المعرّفة في النطاق ده + مرجع للنطاق الأب (parent scope reference). الترتيب ده بيكوّن سلسلة اسمها scope chain.

لمّا الدالة الخارجية بترجع دالة جوّانية بتستخدم متغيّرات من الأم، محرّك JavaScript بيحطّ علامة على الـ environment ده تقول "ممنوع الـ garbage collector يلمسك". الـ environment بيفضل في الذاكرة طول ما في reference نشط للدالة الجوّانية.

المرجع: ECMA-262 §9.1 Lexical Environments — وهي المواصفة الرسمية للغة.

المثال العملي الأول: دالة العدّاد

الـ counter مثال كلاسيكي يبيّن قوة الـ closure. عاوز عدّاد بيزيد بـ 1 كل مرة تنده عليه، ومحدش يقدر يعدّل قيمته من بره.

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

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
console.log(counter.count); // undefined — مفيش وصول مباشر

المتغيّر count محبوس جوّا الـ closure. مفيش طريقة من بره makeCounter توصّلك له. ده encapsulation حقيقي بدون حاجة لـ class أو #private fields.

الاستخدام الحقيقي: متغيّرات خاصة (Private State)

أدراج ملوّنة مغلقة تحاكي فكرة المتغيّرات الخاصة المحمية داخل الـ Closure

قبل ما class تيجي في ES2015، الناس كانت بتعمل OOP في JavaScript بالكامل عن طريق الـ closures. البنك التالي مثال حقيقي:

JavaScript
function createAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) {
      if (amount <= 0) throw new Error("القيمة لازم تكون موجبة");
      balance = balance + amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error("الرصيد مش كافي");
      balance = balance - amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const myAccount = createAccount(1000);
myAccount.deposit(500);   // 1500
myAccount.withdraw(200);  // 1300
myAccount.balance = 0;    // مش هتأثر — مفيش property اسمها balance
console.log(myAccount.getBalance()); // 1300

الـ balance محمي تماماً. الكود من بره مش قادر يعدّله إلا عبر الدوال اللي عرّفناها. ده نفس مبدأ private في Java و C# — بس بدون كلمة private.

الفخ الكلاسيكي: Closures جوّا الـ Loop

الكود ده شائع جداً في كود قديم بـ var:

JavaScript
for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100);
}
// المتوقّع: 0, 1, 2
// الفعلي:   3, 3, 3

السبب: var بيعمل متغيّر واحد على مستوى الدالة كلها. كل callback مخزّن نفس الـ reference لنفس i. لمّا الـ loop يخلص i بقى 3، فالكل بيقرأ 3.

الحل بـ let اللي بيعمل block scope جديد لكل دورة:

JavaScript
for (let i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100);
}
// النتيجة: 0, 1, 2 — كل closure مخزّن نسخته الخاصة

الفرق بين var و let هنا مش رفاهية — ده الفرق بين كود صح وكود غلط في 90% من الحالات.

trade-offs: الذاكرة مش ببلاش

الـ closures بتاكل ذاكرة. كل closure بيحتفظ بنسخة من الـ environment بتاعه. قياس بسيط على Node 22:

JavaScript
const counters = [];
for (let i = 0; i < 1_000_000; i++) {
  counters.push(makeCounter());
}
// قبل: ~12 MB heap
// بعد: ~96 MB heap
// التكلفة التقريبية: 84 بايت لكل closure (بيختلف حسب الـ engine)

لو بتعمل 10 آلاف closure ده مش هيبيّن، لكن لو بتعمل عشرات الملايين الفرق بيتلاحظ. Trade-off واضح: بتكسب encapsulation وstate آمن، بتخسر ذاكرة وبعض التعقيد في الـ debugging.

المرجع للقياس: V8 Blog — Memory Usage and Closures.

متى لا تستخدم Closures

الـ closure مش الحل لكل مشكلة. تجنّبه في الحالات دي:

  • لو State بسيط وعام: متغيّر module-level في ملف اعتيادي أبسط وأرخص في الذاكرة.
  • لو محتاج Inheritance أو Polymorphism: استخدم class العادية. الـ ES Classes فيها #private fields وأبسط في القراءة.
  • في الكود الحرج للأداء (Hot Loops): لو الكود بيتنفّذ ملايين المرّات في الثانية، الـ allocations الإضافية ممكن تظهر في الـ profiler.
  • لو الفريق صغير ومش متعوّد: الكود اللي مفهوم لكل الناس أهم من الكود الأنيق اللي يفهمه واحد بس.

الخطوة التالية

افتح ملف JavaScript عندك دلوقتي، ودوّر على أي let counter = 0; أو let cache = {}; على مستوى الـ module. لفّها جوّا factory function ترجّع object بـ getter وsetter. هتلاحظ إن الـ state بقى محمي وكودك بقى أنضف. لو لاقيت مشكلة، اكتب اسم الملف والسطر في كومنت تحت المقال — هرد على كل واحد.

المصادر

  • ECMA-262 — Lexical Environments (المواصفة الرسمية للغة)
  • MDN — Closures
  • V8 Blog — Garbage Collection و Closures
  • Node.js v22 Release Notes
  • كتاب You Don't Know JS Yet — Scope & Closures لـ Kyle Simpson (Edition 2nd, 2020)

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة