بازگشت به دوره

توابع بازگشتی و توابع IIFE

در این بخش قصد داریم به معرفی دو نوع خاص از توابع بپردازیم که هرچند در این کتاب از آنها استفاده نخواهد شد. اما با این حال آشنایی با این نوع توابع برای تمام برنامه‌نویسان حرفه‌ای ضروری است.

 

توابع بازگشتی (Recursive Functions)

در برنامه‌نویسی می‌توان توابع را طوری تعریف کرد که خودشان را فراخوانی کنند. به تابعی که خود را فراخوانی می‌کند تابع بازگشتی گفته می‌شود. به عنوان مثال تابع زیر یک تابع بازگشتی است.


function recFunction(){
	recFunction();
}

البته تابع فوق در عمل هیچ کاربردی ندارد. زیرا با فراخوانی این تابع، به دلیل فراخوانی‌های مکرر، اجرای تابع (از نظر تئوری) هیچگاه به پایان نخواهد رسید. البته در عمل به دلیل پر شدن حافظه‌ی پشته، پس از مدتی برنامه با خطا مواجه شده و متوقف خواهد شد. تعداد فراخوانی‌هایی که منجر به پر شدن حافظه‌ی پشته می‌شود نیز به محیط اجرا و شرایط زمان اجرا بستگی دارد. (مثلاً در یک آزمایش در مرورگر کروم، پس از حدود ۱۵ هزار فراخوانی برنامه متوقف شد)

اما آیا توابع بازگشتی در عمل کاربردی دارند؟ بله، بسیاری از مسائل را می‌توان با استفاده از توابع بازگشتی حل کرد. البته نه به شکل تابع فوق. بلکه به شکلی که زنجیره‌ی فراخوانی‌ها دارای یک تعداد مشخص و نسبتاً کوچک بوده و تحت شرایط خاصی از ادامه‌ی فراخوانی‌ها جلوگیری شود.

یک مثال معروف از کاربرد توابع بازگشتی، محاسبه‌ی فاکتوریل یک عدد صحیح است. حتماً می‌دانید که فاکتوریل یک عدد صحیح مثبت مانند n از رابطه‌ی زیر قابل محاسبه است.


factorial(n) = n × (n - 1) × (n - 2) × (n - 3) × ... × 2 × 1 

مثلاً فاکتوریل عدد ۵ برابر با ۱۲۰ است که به شکل زیر قابل محاسبه است.


factorial(5) = 5 × 4 × 3 × 2 × 1 = 120

با کمی دقت متوجه می‌شوید که فاکتوریل هر عددی را می‌توان از حاصلضرب همان عدد و فاکتوریل عدد قبلی به دست آورد. مثلاً فاکتوریل عدد ۵ را می‌توان از حاصلضرب عدد ۵ و فاکتوریل ۴ به دست آورد. همینطور فاکتوریل ۴ را می‌توان از حاصلضرب ۴ و فاکتوریل ۳ به دست آورد. این یعنی برای محاسبه‌ی فاکتوریل یک عدد، نیاز به فاکتوریل عددی دیگر داریم. پس تابعی که فاکتوریل عدد n را محاسبه می‌کند، ابتدا باید فاکتوریل عدد n - 1 را محاسبه کند. یعنی این تابع باید خود را برای محاسبه‌ی فاکتوریل عددی دیگر فراخوانی کند. پس می‌توان از توابع بازگشتی برای حل این مسئله استفاده کرد.

نکته‌ی مهم در استفاده از توابع بازگشتی این است که زنجیره‌ی فراخوانی‌ها حتماً باید پس از تعداد مشخصی از فراخوانی‌ها به پایان برسد. یعنی باید شرایط خاصی وجود داشته باشد که با رسیدن به آن شرایط، فراخوانی‌ها متوقف شده و نتیجه‌ی نهایی محاسبه شده و بازگردانده شود.

خوشبختانه در مورد مسئله‌ی فاکتوریل چنین شرایطی وجود دارد. زیرا فاکتوریل عدد ۱ برابر با ۱ است و برای محاسبه‌ی آن نیازی به محاسبه‌ی فاکتوریل عدد دیگری نداریم. در نتیجه زنجیره‌ی فراخوانی‌ها تا رسیدن به عدد ۱ ادامه می‌یابد و در این نقطه متوقف می‌شود. نحوه‌ی نوشتن یک تابع بازگشتی برای محاسبه‌ی فاکتوریل یک عدد صحیح (و مثبت) در قطعه کد زیر نشان داده شده است.


function factorial(n){
	if(n === 1){
		return 1;
	}else{
		return n * factorial(n - 1);
	}
}

حال اگر تابع factorial را مثلاً با ارسال عدد ۳ فراخوانی کنیم. مقدار بازگشتی از تابع در اولین فراخوانی "3 * factorial(2)" خواهد بود. یعنی برای محاسبه‌ی فاکتوریل ۳، ابتدا باید فاکتوریل ۲ محاسبه شود. در نتیجه این تابع برای محاسبه‌ی فاکتوریل ۲ مجدداً فراخوانی می‌شود. مقدار بازگشتی از این فراخوانی‌ "2 * factorial(1)" خواهد بود. پس باید یک بار دیگر این تابع برای محاسبه‌ی فاکتوریل عدد ۱ فراخوانی شود. اما مقدار بازگشتی از این فراخوانی عدد ۱ است. پس در این نقطه زنجیره‌ی فراخوانی‌ها به پایان می‌رسد.

حال مقدار دقیق هر یک از فراخوانی‌ها قابل محاسبه است. اما ترتیب انجام این کار، معکوس ترتیب فراخوانی‌ها خواهد بود. یعنی ابتدا از factorial(1) برای محاسبه‌ی factorial(2) استفاده می‌شود. سپس از factorial(2) برای محاسبه‌ی factorial(3) استفاده می‌شود. قطعه کد زیر نتیجه‌ی اجرای این دستور را نشان می‌دهد.


factorial(3);
← 6
این مثال را می‌توانید اینجا در CodePen اجرا کنید.

البته می‌توان توابع بازگشتی را به صورت غیر بازگشتی نیز پیاده‌سازی کرد. اما در برخی مسائل استفاده از توابع بازگشتی ساده‌تر است. مثلاً تابع فاکتوریل را می‌توان با یک حلقه‌ی for نیز به شکل زیر پیاده‌سازی کرد.


function factorial(n){
	let result = 1;
	for(let i = 1 ; i <= n ; i++){
		result *= i;
	}
	return result;
}
 

توابع IIFE

یک تابع IIFE یا Immediately Invoked Function Expression تابعی است که بلافاصله پس از تعریف، فراخوانی می‌شود. برای تعریف چنین توابعی باید کل تعریف تابع را داخل پرانتز قرار داده و در پایان نیز یک جفت پرانتز قرار داد. قطعه کد زیر نمونه‌ای از تعریف یک تابع IIFE (با تلفط "ایفی") را نشان می‌دهد.


(function(){
	console.log("This is an IIFE");
})();
← "This is an IIFE"

چنین تابعی به صورت خودکار پس از تعریف اجرا می‌شود. با توجه به اینکه این تابع "بی‌نام" است، امکان فراخوانی آن از نقاط دیگر برنامه وجود ندارد. همچنین در صورت نیاز به ارسال آرگومان ورودی به این تابع، می‌توان آرگومان ورودی را در پرانتز پایانی قرار داد. به عنوان مثال در قطعه کد زیر تابع فاکتوریل برای محاسبه‌ی فاکتوریل عدد ۴ به صورت IIFE به کار رفته است.


(function(n){
	let result = 1;
	for(let i = 1 ; i <= n ; i++){
		result *= i;
	}
	console.log(result);
})(4);
← 24
اما توابع IIFE چه کاربردی دارند؟

در واقع از گذشته کاربرد اصلی توابع IIFE، جدا کردن حوزه‌ی شناسه‌ها بوده است. یعنی برای جلوگیری از تداخل شناسه‌ها، از توابع IIFE برای تعریف متغیرهای محلی و موقت استفاده می‌شده است. اما با توجه به اینکه در استاندارد ES6 امکان تعریف شناسه‌ها به صورت Block Scope با استفاده از کلمات کلیدی let و const فراهم شده است، اهمیت توابع IIFE کمتر شده است. البته همچنان می‌توان کاربردهایی را برای این نوع توابع ذکر کرد.

در حال حاضر کاربرد اصلی توابع IIFE، نوشتن بخشی از برنامه در حالت Strict mode است. در فصل قبل دیدیم که در صورت قرار دادن رشته‌ی "use strict" در ابتدای یک فایل جاوا اسکریپت، کل دستورات آن فایل در حالت Strict mode اجرا می‌شوند. اما ممکن است هدف ما فقط اجرا کردن بخشی از کدها در حالت Strict mode باشد. در چنین شرایطی می‌توان از توابع IIFE استفاده کرد. مثلاً می‌توان قطعه کد قبلی را به شکل زیر اصلاح کرد.


(function(n){
	"use strict"
	let result = 1;
	for(let i = 1 ; i <= n ; i++){
		result *= i;
	}
	console.log(result);
})(4);
← 24

در صورتی که این قطعه کد در یک فایل جاوا اسکریپت قرار داشته باشد، فقط همین بخش در حالت Strict mode اجرا خواهد شد. و سایر بخش‌های فایل مورد نظر در حالت عادی اجرا می‌شوند.