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

خاصیت‌ها و متدهای توابع

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

آنچه در این فصل می‌آموزید :  

خاصیت‌ها و متدهای توابع

همانطور که پیش از این نیز اشاره شد، تمام داده‌ها در جاوا اسکریپت به دو دسته‌ی داده‌های اولیه (Primitive) و اشیاء (Objects) تقسیم می‌شوند. یعنی هر داده‌ای که از انواع اولیه نباشد، یک شئ یا Object است. بنابراین، توابع نیز در دسته‌ی اشیاء قرار می‌گیرند، زیرا از انواع داده‌های اولیه نیستند. در نتیجه توابع نیز می‌توانند مانند سایر اشیاء دارای تعدادی خاصیت و متد باشند. در جاوا اسکریپت تمام توابع دارای تعدادی خاصیت و متدِ از پیش تعریف شده هستند. همچنین در صورت نیاز می‌توان به تعداد دلخواه خاصیت و متد به توابع اضافه کرد.

به عنوان مثال یکی از خاصیت‌هایی که به صورت از پیش تعریف شده برای تمام توابع در دسترس است، خاصیت length است. این خاصیت تعداد پارامترهای ورودی توابع را نگهداری می‌کند. مثلاً تابع زیر را در نظر بگیرید.


function square(x) {
	return x ** 2;
}

حال برای مشاهده‌ی مقدار خاصیت length از تابع square، دستور زیر را اجرا کنید.


console.log(square.length);
← 1

مشاهده می‌کنید که عدد 1 در کنسول نمایش داده می‌شود. زیرا تابع square فقط یک پارامتر ورودی دارد.

 

متدهای call و apply

می‌دانیم که در متدهای متعلق به یک شئ خاص، کلمه‌ی کلیدی this به همان شئ اشاره می‌کند. اما در توابعی که متعلق به شئ خاصی نیستند و به صورت سراسری تعریف شده‌اند، کلمه‌ی کلیدی this به شئ window اشاره می‌کند (با این فرض که حالت Strict mode فعال نیست).

اما در برخی مواقع لازم است تا بتوانیم توابع را طوری فراخوانی کنیم که کلمه‌ی کلیدی this در زمان اجرای تابع به یک شئ خاص اشاره کند. برای این منظور می‌توان از متد call استفاده کرد. در واقع تمام توابع در جاوا اسکریپت دارای متدی به نام call هستند که با استفاده از آن می‌توان توابع را طوری فراخوانی کرد که کلمه‌ی کلیدی this در زمان اجرای تابع، به یک شئ خاص اشاره کند.

به عنوان مثال تابع زیر را در نظر بگیرید.


function sayHello(){
	return `Hello, my name is ${ this.name }`;
}

در صورتی که تابع فوق را به صورت عادی فراخوانی کنید، با خروجی زیر مواجه خواهید شد.


console.log(sayHello());
← "Hello, my name is"

همانطور که مشاهده می‌کنید، هیچ مقداری جایگزین this.name نشده است. زیرا در این حالت کلمه‌ی کلیدی this به شئ window اشاره می‌کند و این شئ نیز در حال حاضر خاصیتی به نام name ندارد.

اما می‌توان با استفاده از متد call تابع sayHello را طوری فراخوانی کرد که کلمه‌ی کلیدی this در آن به یک شئ دلخواه اشاره کند. برای این منظور کافی است شئ مورد نظر را به عنوان آرگومان اول به متد call ارسال کنید. به عنوان مثال به قطعه کد زیر توجه کنید.


const clark = { name: 'Clark' };
const bruce = { name: 'Bruce' };

console.log(sayHello.call(clark));
← "Hello, my name is Clark"

console.log(sayHello.call(bruce));
← "Hello, my name is Bruce"

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


function sayHello(greeting){
	return `${ greeting }, my name is ${ this.name }`;
}
console.log(sayHello.call(clark, 'How do you do'));
← "How do you do, my name is Clark"

همچنین توابع در جاوا اسکریپت دارای متد دیگری به نام apply هستند که بسیار شبیه به متد call است. تنها تفاوت بین متدهای call و apply این است که آرگومان دوم متد apply همیشه یک آرایه است. این آرایه شامل تمام آرگومان‌هایی است که باید به تابع اصلی ارسال شوند. یعنی متد apply حداکثر می‌تواند دو آرگومان ورودی داشته باشد که آرگومان اول شیئی است که کلمه‌ی کلیدی this باید به آن اشاره کند و آرگومان دوم نیز آرایه‌ای است که شامل تمام آرگومان‌های ارسالی به تابع اصلی است.

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


function sayHello(greeting){
	return `${ greeting }, my name is ${ this.name }`;
}
console.log(sayHello.apply(clark, ['How do you do']));
← "How do you do, my name is Clark"
 

خاصیت‌ها و متدهای سفارشی

هیچ محدودیتی برای اضافه کردن خاصیت‌ها و متدهای سفارشی دلخواه به توابع وجود ندارد. به عنوان مثال شما می‌توانید یک خاصیت به نام description به هر یک از توابع موجود در برنامه اضافه کنید تا توضیح کوتاهی در مورد عملکرد تابع را در خود نگهداری کند. مثلاً در مورد تابع square می‌توان به صورت زیر عمل کرد.


square.description = 'Squares a number that is provided as an argument'
 

ذخیره‌سازی خروجی توابع در خاصیت‌ها (Memoization)

با استفاده از خاصیت‌های سفارشی اضافه شده به توابع، می‌توان یک قابلیت بسیار مفید و کاربردی را به توابع اضافه کرد که به Memoization مشهور است. فرض کنید تابعی داریم که برای محاسبه و بازگرداندن مقدار خروجی، زمان زیادی را صرف می‌کند. حال اگر تعداد فراخوانی‌های این تابع در برنامه زیاد باشد، اجرای برنامه بسیار کند انجام خواهد شد.

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

به عنوان مثال فرض کنید تابع suqare زمان زیادی را برای محاسبه‌ی توان دوم یک عدد صرف می‌کند (البته می‌دانیم که در عمل چنین نیست). برای اضافه کردن قابلیت Memoization به این تابع، می‌توان آن را به شکل زیر بازنویسی کرد.


function square(x){
	square.cache = square.cache || {};
	if (!square.cache[x]) {
		square.cache[x] = x ** 2;
	}
	return square.cache[x]
}

در این صورت در اولین فراخوانی تابع square، خاصیتی به نام cache به این تابع اضافه می‌شود که در ابتدا یک شئ تهی است. سپس بررسی می‌شود که آیا خاصیت cache (که خود یک شئ است) خاصیتی به نام x دارد یا خیر؟ در صورتی که چنین خاصیتی از قبل وجود نداشته باشد، مقدار خروجی متناظر با مقدار x محاسبه شده و در خاصیت x از شئ cache ذخیره می‌شود.

اما اگر خاصیتی به نام x از قبل در شئ cache وجود داشته باشد، یعنی نیازی به محاسبه‌ی مجدد خروجی نیست و مقدار خروجی متناظر با x از قبل در شئ cache ذخیره شده است که در خط آخر از این تابع، همین مقدار ذخیره شده به عنوان نتیجه از تابع square بازگردانده می‌شود.

البته این یک مثال بسیار ساده بود که استفاده یا عدم استفاده از Memoization تاثیر چندانی در سرعت اجرای برنامه ندارد. اما در برخی توابع که محاسبه‌ی مقدار خروجی شامل مراحل زیادی بوده و زمان زیادی را صرف می‌کند، استفاده از Memoization می‌تواند تاثیر زیادی در سرعت اجرای برنامه داشته باشد.