توابع بازگشتی در جاوااسکریپت: راهنمای سریع با مثالهای کاربردی
۱۴۰۳/۴/۲۹
عمل بازگشت یا Recursion یک تکنیک حل مساله در برنامهنویسی است. در این مقاله شما چگونگی استفاده از توابع بازگشتی را در جاوااسکریپت یاد خواهید گرفت.
تابع بازگشتی چیست؟
یک تابع بازگشتی به تابعی گفته میشود که جایی در درون بدنهی خود، خودش را صدا میزند. در زیر یک مثال ابتدایی از یک تابع بازگشتی وجود دارد:
function recursiveFunc() {
// some code here...
recursiveFunc()
}
همانطور که میبینید، تابع recursiveFunc
درون بدنهی خودش صدا زده شده. این باعث میشود که روند اجرای تابع تا زمانی که خروجی مورد نظر به دست آید، تکرار شود.
خب... حالا چجوری به تابع بفهمونیم که چه زمانی متوقف بشه؟ شما میتونید این کار رو با یه شرط پایه انجام دهید.
سه بخش از یک تابع بازگشتی
هر زمانی که یک تابع بازگشتی مینویسیم، سه اصل باید برقرار باشد:
- تعریف تابع
- شرط پایه
- فراخوانی بازگشتی
وقتی این سه اصل در نظر گرفته نشود، تابع بازگشتی شما به درستی کار نخواهد کرد. بیاین یکم دقیقتر هر مورد رو بررسی کنیم.
چگونه یک تابع بازگشتی تعریف کنیم
شما میتونید یک تابع بازگشتی رو همونطور تعریف کنید که یک تابع عادی رو در جاوااسکریپت تعریف میکنید.
function recursiveFunc() {
// some code here...
}
آنچه توابع بازگشتی را از توابع معمولی جاوااسکریپت متمایز میکند همان شرط پایهای و فراخوانی درونی است که به آن اشاره کردیم.
شرط پایه چیه؟
وقتی از یک تابع بازگشتی استفاده میکنیم، شرط پایه چیزیه که به تابع این اجازه رو میده که بفهمه چه زمانی عمل فراخوانی خودش رو متوقف کنه. وقتی که شرط در نظر گرفته محرز شد، عملیات فراخوانی بازگشتی به اتمام میرسد.
function recursiveFunc() {
if(/* شرط پایه */) {
// stops recursion if condition is met
}
// else recursion continues
recurse();
}
چرا به این شرط پایه نیاز داریم؟
بدون وجود این شرط، با بازگشت بینهایت مواجه خواهیم شد. وضعیتی که تابع ما را به فراخوانی خودش به صورت بینهایت مجبور میکنه. چیزی شبیه به این:
function doSomething(action) {
console.log(`I am ${action}.`)
doSomething(action)
}
doSomething('running')
همچنین بدون شرط پایه، بینهایت اجرا شدن این تابع باعث میشود که برنامه ما با خطای Maximum call stack exceeded when there's no base condition مواجه شود.
وظیفهی Call Stack اینه که پیگیری بکنه که کدام توابع و البته تابعهای درونی آنها در حال اجرا هستند. مسلما Call Stack دارای محدودیت است و از آنجایی که یک تابع بازگشتی بدون شرط بصورت نامحدود فراخوانی میشود، به محدودیت Call Stack برمیخورد.
شرط پایه در واقع راهی است برای شکستن جریان فراخوانی مداوم تابع، هنگامی که به خروجی مورد نظر رسیدیم.
مثالی از تابع بازگشتی
بیاین این مثال ساده از یک تابع بازگشتی رو بررسی کنیم.
function doSomething(n) {
if (n === 0) {
console.log('TASK COMPLETED!')
return
}
console.log("I'm doing something.")
doSomething(n - 1)
}
doSomething(3)
شرط پایه برای تابع doSomething
این است که n === 0
باشد. هر زمان که تابع فراخوانی میشود، ابتدا بررسی میکند که آیا شرط پایه محرز شده است یا خیر.
اگر شده بود، پیغام TASK COMPLETED!
را چاپ میکند. اگر نه، به اجرای باقی کدهای تابع ادامه میدهد. در این مورد، پیام I'm doing something
را چاپ کرده و سپس دوباره تابع را فراخوانی میکند.
فراخوانی بازگشتی
فراخوانی بازگشتی همان چیزی است که باعث میشود تابع دوباره خود را فراخوانی کند. در تابع doSomething
، فراخوانی بازگشتی خط زیر است.
doSomething(n - 1)
توجه کنید که وقتی تابع خود را فراخوانی میکند، چه اتفاقی میافتد. یک پارامتر جدید n - 1
به تابع پاس داده میشود. در هر تکرار یک فراخوانی بازگشتی، پارامتر با پارامتر فراخوانی قبلی متفاوت خواهد بود.
تابع به فراخوانی خود ادامه میدهد تا زمانی که پارامتر جدید شرط پایه را محرز کند.
تفاوت بازگشت (Recursion) در مقابل حلقهها (Loops)
بازگشت و حلقهها به روشهای مشابهی کار میکنند. هر تابع بازگشتی که مینویسید دارای یک راهحل جایگزین با استفاده از حلقه است. به عنوان مثال، میتوانید یک تابع برای پیدا کردن فاکتوریل یک عدد مشخص با استفاده از هر دو روش بازگشتی و حلقه ایجاد کنید.
نحوه پیادهسازی فاکتوریل با استفاده از حلقه:
function findFactorial(num) {
let factorial = 1
for (let i = num; i > 0; i--) {
factorial *= i
}
return factorial
}
findFactorial(5) // 120
برای پیدا کردن فاکتوریل با استفاده از حلقه، ابتدا یک متغیر به نام factorial
با مقدار 1 مقداردهی اولیه میکنید. سپس حلقه را با عدد داده شده num
آغاز کنید. حلقه تا زمانی که i > 0
باشد به اجرا ادامه میدهد. در هر تکرار، مقدار فعلی factorial
را در i
ضرب میکنید. و مقدار i
را به میزان 1 کاهش میدهید تا زمانی که i
بزرگتر از صفر نباشد. در نهایت، زمانی که حلقه به پایان رسید، مقدار فاکتوریل را بازمیگردانید.
چگونه فاکتوریل را با روش بازگشتی پیادهسازی کنیم:
میتوانید همان راهحل را با یک تابع بازگشتی ایجاد کنید.
function findFactorial(num) {
if (num === 0) return 1
let factorial = num * findFactorial(num - 1)
return factorial
}
findFactorial(5) // 120
اول، شما به یک شرط پایه مانند num === 0
نیاز دارید.
همچنین به فراخوانی بازگشتی findFactorial(num - 1)
نیاز دارید تا اطمینان حاصل کنید که عدد در هر فراخوانی با پارامتر جدید n-1
کاهش مییابد. سپس نتیجه را با عدد قبلی num * findFactorial(num - 1)
ضرب میکنید تا زمانی که شرط پایه برآورده شود.
حالا کدوم بهتره؟ بازگشتی یا حلقهها؟
پاسخ درست یا غلطی برای این سوال وجود ندارد. تصمیم با شماست. بسته به مسالهای که در حال حل آن هستید، ممکن است یکی رو به دیگری ترجیح بدید.
کد خود را برای خوانایی بهتر، بهینهسازی کنید. گاهی اوقات مانند مثال فاکتوریل، بازگشت منجر به کدی کوتاهتر و خواناتر میشود. اما توابع بازگشتی همیشه هم بهترین گزینه نیست و ممکن است زمان زیادی را از سیستم بگیرد. به همین جهت باید در استفاده از آنها دقت لازم را داشت.
نتیجهگیری
خواندن و نوشتن توابع بازگشتی ممکن است در ابتدا گیجکننده باشد. اما به یاد داشته باشید، چیزی که توابع بازگشتی را از توابع معمولی متمایز میکند شرط پایه و فراخوانی بازگشتی است.
توابع بازگشتی در جاوااسکریپت به عنوان یکی از مفاهیم مهم و کاربردی در برنامهنویسی شناخته میشوند. این توابع به برنامهنویسان اجازه میدهند تا مسائل پیچیده را به صورت سادهتر و با کدهایی کوتاهتر و خواناتر حل کنند. یکی از کاربردهای اصلی توابع بازگشتی در جاوااسکریپت، حل مسائل بازگشتی نظیر محاسبه فاکتوریل، اعداد فیبوناچی و جستجو در ساختارهای دادهای پیچیده مانند درختها و گرافها است.
توابع بازگشتی چه کاربردهایی میتوانند داشته باشند؟
توابع بازگشتی به ویژه در مواقعی که ساختار دادهای دارای طبیعت تکراری است، بسیار مفید واقع میشوند. به عنوان مثال، در عملیاتهای مربوط به درختهای دودویی، توابع بازگشتی میتوانند به طور موثر برای پیمایش و انجام عملیاتهای مختلف بر روی گرههای درخت استفاده شوند. این توابع همچنین در پردازش دادههای تودرتو و سلسلهمراتبی، مانند منوهای تودرتو یا فایل سیستمها، کاربرد دارند.
یکی دیگر از کاربردهای مهم توابع بازگشتی در حل مسائل تقسیم و غلبه (Divide and Conquer) است. الگوریتمهایی مانند مرتبسازی سریع (QuickSort) و مرتبسازی ادغامی (MergeSort) به طور گسترده از بازگشت برای تقسیم مساله به زیرمسائل کوچکتر و حل آنها استفاده میکنند. این رویکرد نه تنها باعث سادهتر شدن کد میشود، بلکه در بسیاری از موارد به بهبود کارایی و کاهش پیچیدگی زمانی الگوریتم نیز کمک میکند.
به طور کلی، توابع بازگشتی ابزاری قدرتمند برای برنامهنویسان جاوااسکریپت هستند که میتوانند به سادگی و با کدی خوانا، مسائل پیچیده را حل کرده و برنامههایی کارآمدتر و قابل نگهداریتر بنویسند. با این حال، استفاده صحیح از آنها نیازمند درک عمیق از مفاهیم بازگشت و مدیریت مناسب شرطهای پایه است تا از وقوع خطاهای بازگشتی بینهایت و مصرف بیرویه منابع جلوگیری شود.
منبع