مستوى المقال: مبتدئ
لو شفت دالة JavaScript بتنده نفس اسمها جوّاها، ده مش غلط ولا bug. ده Recursion. في 7 دقايق هتفهم ليه الكود ده مش بيدخل في حلقة لا نهائية، وامتى تستخدمه بدل الـ for loop بالظبط.
المشكلة باختصار
أول مرة بتشوف دالة بتنده نفسها، الدماغ بترفض الفكرة. لأن لو الدالة بتنده نفسها، يبقى المفروض تفضل بتنده نفسها للأبد. الإجابة الصح: لأ، بشرط واحد بس. لو فهمت الشرط ده، Recursion هيبقى أبسط من الـ loops في حالات معينة — زي تصفّح مجلدات أو شجرة JSON أو DOM.
Recursion: لما الدالة تكون أبسط من الحلقة
مثال للمبتدئ — دمى ماتريوشكا
تخيّل عندك دمية ماتريوشكا روسية. بتفتحها، بتلاقي جوّاها دمية أصغر. بتفتح الصغيرة، بتلاقي جوّاها أصغر. بتفضل تفتح كل واحدة بنفس الخطوة بالظبط — لحد ما توصل لدمية صغيرة جدًا مش بتتفتح. هنا بتقف.
الـ recursion بنفس المنطق. الدالة بتعمل خطوة بسيطة على المدخل، وبعدين بتنده نفسها على نسخة أصغر من نفس المشكلة. بتفضل بتنده نفسها لحد ما توصل لحالة بسيطة جدًا الإجابة فيها معروفة على طول. الحالة دي اسمها base case. لو نسيت تكتبها، آه — هيدخل في حلقة لا نهائية فعلًا، والـ Node هيرميلك RangeError: Maximum call stack size exceeded بعد ثانية.
التعريف العلمي
Recursion هي تقنية تنفيذ بتنحلّ فيها مشكلة عبر استدعاء الدالة لنفسها على نسخة أصغر من نفس المشكلة. كل recursion صحيح بيلتزم بشرطين بدون استثناء:
- Base case: حالة بسيطة الدالة بترجع فيها قيمة فورًا بدون ما تنده نفسها.
- Recursive case: استدعاء للدالة على input أصغر بحيث إنه بيقترب من الـ base case في كل خطوة.
كل استدعاء للدالة بيتسجّل في حتة في الذاكرة اسمها call stack. كل إطار (frame) في الـ stack بيحتفظ بقيم المتغيرات بتاعت الاستدعاء ده لحد ما يخلّص. لما الـ base case يرد قيمة، الـ stack بيبدأ يفك من فوق لتحت، وكل استدعاء بياخد قيمة الاستدعاء اللي تحته ويكمّل حسابه.
أبسط مثال كود — factorial
factorial(5) معناها 5 × 4 × 3 × 2 × 1 = 120. كود JavaScript شغّال على Node 22:
function factorial(n) {
// base case: لو وصلنا لـ 1 ارجع مباشرة
if (n <= 1) return 1;
// recursive case: ضرب n في factorial(n-1)
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
console.log(factorial(10)); // 3628800اللي بيحصل لما تنده factorial(5) بالتفاصيل:
factorial(5)محتاجfactorial(4)عشان يكمّل، فبيتحط فوق الـ stack وبيستنى.factorial(4)محتاجfactorial(3)— نفس القصة.factorial(3)محتاجfactorial(2).factorial(2)محتاجfactorial(1).factorial(1)بيرجّع1على طول. ده الـ base case.- الـ stack بيرجع لتحت بالترتيب: 1 ثم 1×2=2 ثم 2×3=6 ثم 6×4=24 ثم 24×5=120.
مثال عملي تاني — تصفّح مجلدات
عايز تطبع كل الملفات جوّا مجلد، حتى لو فيه مجلدات فرعية متداخلة 10 مستويات. مع loop عادي هتحتاج تتبع المستويات يدويًا بـ stack أو queue. مع recursion، الكود في 9 سطور:
import { readdirSync, statSync } from "node:fs";
import { join } from "node:path";
function listFiles(dir) {
for (const name of readdirSync(dir)) {
const fullPath = join(dir, name);
if (statSync(fullPath).isDirectory()) {
listFiles(fullPath); // الدالة بتنده نفسها على المجلد الفرعي
} else {
console.log(fullPath);
}
}
}
listFiles("./project");لاحظ هنا: مفيش if صريح للـ base case. الـ base case ضمني — لما المجلد ميكونش جوّاه أي مجلدات فرعية، الـ for ميدخلش في الفرع التاني، والدالة بترجع طبيعي. ده pattern شائع في الكود الإنتاجي.
الفرق بين Recursion والـ Loop
أي recursion بتقدر تكتبه كـ loop، والعكس صحيح. السؤال: امتى يكون كل واحد فيهم أنضف؟
- Recursion أحسن لما: البيانات شجرية أو متداخلة (مجلدات على القرص، DOM tree، JSON nested، graphs، شجرة تعليقات في تطبيق سوشيال).
- Loop أحسن لما: البيانات خطّية وعدد التكرار معروف أو محدود (array بسيط، عدّاد، iteration على كل مستخدم في DB).
الـ trade-off هنا: كل استدعاء recursive بياكل من الـ call stack. في Node.js (V8) الحد الافتراضي حوالي 10,000 إطار قبل ما تشوف RangeError. الـ for loop ما عندوش الحد ده — بيعدّ على array من 10 مليون عنصر بدون مشكلة. كمان كل استدعاء فيه overhead بسيط لإنشاء الـ frame، فلو الـ hot path بتاعك بيتنفّذ مليون مرة في الثانية، الفرق يبقى محسوس.
متى لا تستخدم Recursion
- لو العمق ممكن يعدّي 5,000 استدعاء وانت في بيئة JavaScript — لأن V8 لسه مفيهوش tail call optimization حقيقي رغم إن الـ ES2015 spec بيدعمها.
- لو المشكلة خطّية بحتة (مجموع array من 100 مليون رقم — استخدم loop عادي).
- في hot paths بتنفّذ مليار استدعاء/ثانية — تكلفة الـ stack frame بتظهر في الـ profiler.
- لو الـ input جاي من المستخدم وممكن يكون عميق — مهاجم ممكن يبعت JSON عمقه 100,000 ويضرب السيرفر بـ stack overflow.
الخطوة التالية
افتح محرر الكود واكتب دالة countDown(n) بتطبع الأرقام من n لحد 1 باستخدام recursion، بدون أي loop. لو نسيت الـ base case، Node هيقولك Maximum call stack size exceeded بعد أقل من ثانية. التمرين ده 3 دقايق وهيخلّي المفهوم يلصق فعلًا — لأن أحسن طريقة تفهم recursion إنك تعمل واحد بنفسك وتشوف ايه اللي بيكسر لما الـ base case غايب.
مصادر
- MDN Web Docs — JavaScript Functions and Recursion: developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions
- V8 Blog — Proper Tail Calls in JavaScript: v8.dev/blog/modern-javascript
- Node.js docs — V8 stack size and
--stack-sizeflag: nodejs.org/api/cli.html - ECMAScript Language Specification — Function Calls and Execution Contexts: tc39.es/ecma262