ما هي دالة الـ callback في جافا سكريبت؟
في جافا سكريبت، الدوال عبارة عن قيم مثل أي قيمة أخرى. تقدر تخزّنها في متغيرات، وتحطّها داخل مصفوفات، والأهم هنا: تمرّرها كوسائط (arguments) لدوال ثانية. ولمّا تمرّر دالة لدالة أخرى عشان تستدعيها لاحقًا، الدالة اللي مرّرتها تُسمّى callback.
greet ما يهمّه ولا يعرف أصلاً إيش بيسوّي formatter. كل اللي يسويه إنه يناديه ويمرّر له اسم، ثم يستخدم الناتج. السلوك أنت اللي تتحكم فيه عبر تمرير دوال callback مختلفة. وهذي المرونة هي بالضبط سبب وجود الـ callbacks.
الـ callbacks المتزامنة تنفّذ حالاً
ليست كل دوال الـ callback غير متزامنة. في الحقيقة، كثير من دوال المصفوفات اللي تستخدمها يوميًا مبنية على فكرة الـ callback، وتنفّذ الدالة الممرّرة بشكل متزامن — أي قبل ما ترجع الدالة الخارجية نتيجتها:
دوال map وfilter وreduce كلها تستقبل دالة callback وتستدعيها مرة واحدة لكل عنصر، فورًا وفي نفس اللحظة. فبمجرد أن ينتهي map من التنفيذ، تكون كل استدعاءات الـ callback قد حدثت فعلًا. لا شيء مؤجَّل لوقت لاحق.
هذا نمط عادي لما يُعرف بـ higher-order function — "خذ هذه المهمة، وهذه طريقة تنفيذها، وأعِد لي النتيجة." لا علاقة لـ event loop بالأمر هنا.
دوال callback غير المتزامنة تعمل لاحقًا
الـ callbacks التي يقصدها الناس عادةً حين يقولون "callback" هي النوع غير المتزامن (async). أنت تُمرِّر دالة إلى واجهة برمجية تستغرق وقتًا — مؤقِّت، أو طلب شبكة، أو قراءة ملف — ثم تقوم هذه الواجهة باستدعاء دالتك بعد أن ينتهي العمل.
ترتيب الإخراج: قبل ثم بعد، وبعد ثانية واحدة يظهر تم تشغيل المؤقت. الدالة setTimeout لا توقف تنفيذ البرنامج، بل تسلّم الـ callback إلى بيئة التشغيل ثم ترجع فوراً، ويكمل بقية السكربت عمله بشكل طبيعي. وبعد مرور ثانية، يلتقط event loop الـ callback ويشغّله.
هذا النمط، "ارجع الآن ونادِ لاحقاً"، هو النموذج الذهني الذي يحكم كل واجهات async callback في جافا سكريبت، بدءاً من addEventListener وصولاً إلى واجهات الملفات القديمة في Node.js.
نمط error-first callback في Node.js
قبل ظهور Promises، اعتمد Node.js شكلاً موحّداً للـ callback: الوسيط الأول يكون الخطأ (أو null في حال عدم وجوده)، أما باقي الوسائط فهي النتيجة الفعلية. ستصادف هذا النمط كثيراً في الكود القديم وفي بعض المكتبات حتى اليوم.
المتلقّي يفحص err أولاً ويخرج مباشرةً إذا كانت قيمته صادقة، وبعدها فقط يعتمد على النتيجة. هذه مجرد قاعدة متعارف عليها، ولا تفرضها اللغة نفسها، لكن بمجرد أن ترى التوقيع (err, result) => ... ستتعرّف عليه في كل مكان.
جحيم الـ Callback (Callback Hell)
تبدأ المشكلة حين تعتمد خطوة غير متزامنة على نتيجة خطوة أخرى. عندها يضطر كل callback للتداخل داخل الذي سبقه، فينتهي بك الأمر أمام كود يتدرّج كالسُّلّم:
هذه هي ما تُعرف بـ "هرم الهلاك" أو ما يُسمّى callback hell. وهناك عدّة أسباب تجعل هذا النمط مزعجًا:
- تدفّق التحكّم يتعرّج بدل أن يسير من الأعلى إلى الأسفل بشكل مقروء.
- كل مستوى يُكرّر نفس السطر المملّ
if (err) return .... - إذا رمت إحدى دوال الـ callback استثناءً، فإنه لا ينتقل إلى الدوال الخارجية، فتضطر للتعامل مع الأخطاء في كل طبقة على حدة.
- أي إعادة هيكلة للكود تعني إعادة ضبط المسافات البادئة للكتلة كاملة.
يمكنك تخفيف المشكلة نسبيًا باستخراج دوال مُسمّاة، لكن أصل المشكلة — وهو أنّ تركيب العمليات غير المتزامنة باستخدام callbacks الخام عمليةٌ مرهقة — يبقى قائمًا. وهذه تحديدًا هي المشكلة التي جاءت الـ Promises لحلّها.
مزلقان ينبغي الانتباه لهما
احذر من استدعاء الـ callback عن طريق الخطأ. عندما تُمرّر دالة callback، فأنت تُمرّر الدالة نفسها — لا ناتج استدعائها.
انتبه من this. لو دالة الـ callback عندك دالة عادية وبتستخدم this، فإن قيمة this بتتحدد حسب طريقة استدعاء الدالة، مش حسب المكان اللي اتعرّفت فيه. أما الدوال السهمية (arrow functions) فبتتخطى المشكلة دي لأنها بتورث this من النطاق المحيط بيها:
برمجة الدوال السهمية هي الخيار الافتراضي للـ callback المضمّنة لهذا السبب بالذات.
الفرق بين callback وPromise في جافا سكريبت
لا تزال دوال الـ callback حاضرة في الواجهات المتزامنة مثل (map, forEach, sort)، وفي مستمعات الأحداث مثل (element.addEventListener("click", ...))، وفي خطافات البيئة منخفضة المستوى. أما بالنسبة للعمليات غير المتزامنة التي تُنتج نتيجة واحدة، فقد انتقلت المنظومة بالكامل تقريباً إلى الـ Promises.
مقارنة سريعة:
- callbacks — مباشرة وبسيطة، لكنها سيئة في التركيب. التعامل مع الأخطاء يدوي في كل خطوة.
- Promises — قيمة تمثّل نتيجة مستقبلية. تربطها بالتسلسل عبر
.then()، وتتعامل مع الأخطاء مرة واحدة عبر.catch()، فيختفي شكل الهرم.
مع ذلك، يبقى فهم الـ callback ضرورياً: فهي الأساس الذي بُنيت عليه الـ Promises، وهي موجودة في كل مكان داخل الكود المبني على الأحداث. لكن نادراً ما تكتب اليوم واجهات غير متزامنة جديدة باستخدام callback خام.
التالي: Promises
تأخذ الـ Promises فكرة "نفّذ هذا عندما يصبح ذاك جاهزاً" وتغلّفها داخل كائن يمكنك تمريره وربطه بالتسلسل وتركيبه. هذا هو موضوع الصفحة التالية — وهي الجسر الذي يوصلك إلى async/await، الطريقة التي تتعامل بها جافا سكريبت الحديثة مع معظم الأعمال غير المتزامنة.
الأسئلة الشائعة
ما هي دالة callback في JavaScript؟
الـ callback ببساطة هي دالة تمرّرها كوسيط لدالة أخرى، لتقوم هذه الأخيرة باستدعائها لاحقاً عند الحاجة. مثلاً في setTimeout(() => console.log('hi'), 1000) أنت تمرّر دالة سهمية كـ callback، فتحتفظ بها setTimeout وتستدعيها عند انتهاء المؤقّت. الـ callbacks كانت الطريقة الأصلية التي تعاملت بها JavaScript مع منطق «نفّذ هذا عندما يصبح ذاك جاهزاً».
ما الفرق بين الـ callback المتزامن وغير المتزامن؟
الـ callback المتزامن (synchronous) يُنفَّذ مباشرة أثناء استدعاء الدالة التي استقبلته. مثلاً [1, 2, 3].map(x => x * 2) يستدعي الـ callback ثلاث مرات قبل أن تعود map أصلاً. أما غير المتزامن (asynchronous) فيُحفَظ ويُستدعى لاحقاً بعد وقوع حدث ما، كما في setTimeout و fs.readFile ومستمعي أحداث الـ DOM. الـ callbacks غير المتزامنة لا توقف تنفيذ باقي الكود.
ما هو callback hell وكيف نتجنّبه؟
الـ callback hell هو ذلك الشكل الهرمي (المعروف بـ pyramid of doom) الذي ينتج عندما تعتمد callbacks غير متزامنة بعضها على بعض فتتداخل على عدة مستويات. تصبح متابعة تدفّق الكود ومعالجة الأخطاء كابوساً حقيقياً. الحل هو استخدام Promises مع سلاسل .then()، والأفضل من ذلك async/await — كلاهما يفرد الهرم ويحوّله إلى كود مسطّح وقابل للقراءة.