۲۵ اکتبر ۲۰۲۲

توابع (Functions)

اغلب اوقات ما نیاز داریم یک مجموعه‌ای از دستورها را در خیلی از جاهای کد چندین بار اجرا کنیم.

برای مثال، می‌خواهیم که پیغامی زیبا برای کسی که وارد صفحه‌ای می‌شود یا خارج می‌شود یا جاهایی دیگر نمایش دهیم.

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

ما مثال‌هایی از توابع درون سیستمی مثل alert(message)، prompt(message, default) و confirm(question) را دیده‌ایم. اما میتوانیم توابع خودمان را هم بسازیم.

تعریف توابع (Function Declaration)

برای ساختن یک تابع ما به تعریف کردن تابع نیاز خواهیم داشت.‌(function declaration)

چیزی شبیه کد پایین:

function showMessage() {
  alert( 'Hello everyone!' );
}

کلمه‌ی کلیدی function اول می‌آید، سپس اسم تابع و سپس لیستی از پارامترها داخل پرانتز (با کاما جدا می‌شوند، در مثال بالا داخل پرانتزها خالی‌ست) و در نهایت کد تابع، با نام “بدنه‌ی تابع”، که توسط دو براکت محصور شده است.

function name(parameter1, parameter2, ... parameterN) {
 // body
}

تابع جدید ما می‌تواند با اسمش صدا زده شود: showMessage().

برای نمونه:

function showMessage() {
  alert( 'Hello everyone!' );
}

showMessage();
showMessage();

فراخوانی showMessage() کد درون تابع را اجرا می‌کند. در اینجا ما پیغام را دوبار خواهیم دید.

این مثال یکی از اهداف اصلی توابع را نشان می‌دهد: اجتناب از کد تکراری.

اگر ما نیاز داشته باشیم نحوه‌ای که پیغام نشان داده می‌شود را عوض کنیم، تنها لازم است که کد را در یک قسمت تغییر دهیم: تابعی که آن را خروجی می‌دهد.

متغیرهای محلی (Local variables)

اگر یک متغیر در درون تابع تعریف شود، فقط در درون همان تابع قابل استفاده است.

برای نمونه:

function showMessage() {
  let message = "Hello, I'm JavaScript!"; // local variable

  alert( message );
}

showMessage(); // Hello, I'm JavaScript!

alert( message ); // <-- Error! The variable is local to the function

متغیرهای بیرونی (Outer variables)

یک تابع می‌تواند به متغیر درونی دسترسی داشته باشد، به عنوان مثال:

let userName = 'John';

function showMessage() {
  let message = 'Hello, ' + userName;
  alert(message);
}

showMessage(); // Hello, John

تابع دسترسی کامل به متغیر بیرونی دارد. همینطور میتواند آنرا تغییر هم بدهد.

برای مثال:

let userName = 'John';

function showMessage() {
  userName = "Bob"; // (1) changed the outer variable

  let message = 'Hello, ' + userName;
  alert(message);
}

alert( userName ); // John before the function call

showMessage();

alert( userName ); // Bob, the value was modified by the function

متغیر بیرونی فقط در مواقعی مورد استفاده قرار می‌گیرد که متغیر محلی‌ای وجود نداشته باشد.

اگر یک متغیر هم‌نام در درون تابع تعریف شود، جانشین متغیر بیرونی می‌شود. برای مثال، در کد زیر، تابع از متغیر محلی userName استفاده می‌کند و متغیر بیرونی نادیده گرفته می‌شود:

let userName = 'John';

function showMessage() {
  let userName = "Bob"; // تعریف یک متغیر محلی

  let message = 'Hello, ' + userName; // Bob
  alert(message);
}

// the function will create and use its own userName
showMessage();

alert( userName ); // John, unchanged, the function did not access the outer variable
متغیرهای سراسری (Global Variables)

متغیرهای تعریف شده بیرون از هر تابعی، مثل userName در کد بالا، سراسری نامیده می‌شوند.

متغیرهای سراسری برای هر تابعی قابل استفاده است (مگر اینکه متغیری محلی آن را تغییر دهد).

معمولا، یک تابع تمام متغیرهای مربوط به کارش را تعریف می‌کند. متغیرهای سراسری فقط اطلاعات سطح-پروژه را ذخیره می‌کنند و مهم است که این متغیرها قابل دسترسی از هرجایی باشند. کدهای جدید متغیرهای سراسری کمی دارند یا اصلا ندارند. اکثر متغیرها در درون تابع‌ هایشان تعریف می‌شوند.

پارامترها

ما می‌توانیم اطلاعات دلخواهی را به توابع با کمک پارامترها پاس بدهیم.

در مثال زیر، تابع دو پارامتر دارد: from و text.

function showMessage(from, text) { // parameters: from, text
  alert(from + ': ' + text);
}

showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)

وقتی تابع در خطوط (*) و (**) صدا زده می‌شود، مقادیر داده شده در متغیرهای محلی from و text کپی می‌شوند. سپس، تابع از آنها استفاده می‌کند.

مثالی دیگر: یک متغیر from داریم و به تابع پاس می‌دهیم. توجه کنید: تابع، from را تغییر می‌دهد، اما تغییر در بیرون دیده نمی‌شود، چراکه تابع همیشه یک کپی از مقدار آن را می‌گیرد:

function showMessage(from, text) {

  from = '*' + from + '*'; // make "from" look nicer

  alert( from + ': ' + text );
}

let from = "Ann";

showMessage(from, "Hello"); // *Ann*: Hello

// the value of "from" is the same, the function modified a local copy
alert( from ); // Ann

وقتی یک مقدار به عنوان یک پارامتر تابع پاس داده می‌شود، به آن آرگومان نیز می‌گویند.

به عبارتی دیگر، بگذارید این مقررات را تعیین کنیم:

  • یک پارامتر یک متغیر لیست شده درون پرانتز‌های تعریف تابع است (یک عبارت مخصوص زمان تعریف).
  • یک آرگومان مقداری‌ست که به تابع موقع صدازدن آن پاس داده شده است (یک عبارت مخصوص زمان فراخوانی).

ما توابع را با پارامترهای‌شان تعریف می‌کنیم، و سپس آن‌ها را با آرگومان‌هایشان صدا می‌زنیم.

در مثال بالا، می‌توانید بگویید: «تابع ‍sayMessage با دو پارامتر تعریف شده، پس با دو آرگومان صدا زده می‌شود: from و Hello».

مقادیر پیش‌فرض

اگر پارامتری فراهم نشده باشد، مقادیر آن undefined می‌شوند.

برای مثال، تابع showMessage(from, text)، می‌تواند با یک آرگومان صدا زده شود:

showMessage("Ann");

این یک خطا نیست. خروجی این فراخوانی "Ann: undefined" است. text نداریم پس پیش‌فرض این است که text === undefined.

اگر ما بخواهیم یک مقدار “پیش‌فرض” برای text در این حالت استفاده کنیم، می‌توانیم آن را بعد از = مشخصش کنیم:

function showMessage(from, text = "no text given") {
  alert( from + ": " + text );
}

showMessage("Ann"); // Ann: no text given

حالا اگر پارامتر text پاس داده نشود، مقدار "no text given" را می‌گیرد.

همچنین اگر پارامتر وجود داشته باشد نیز ممکن است مقدار پیش‌فرض قرار بگیرد، در صورتی که برابر با undefined باشد، مانند زیر:

showMessage("Ann", undefined); // Ann: no text given

اینجا "no text given" یک رشته است، اما می‌تواند عبارت پیچیده‌تری باشد، که تنها در حالتی ارزیابی و مقداردهی می‌شود که پارامتری وجود نداشته باشد. بنابراین این هم ممکن است:

function showMessage(from, text = anotherFunction()) {
  // anotherFunction() only executed if no text given
  // its result becomes the value of text
}
ارزیابی پارامترهای پیش‌فرض

در جاوااسکریپت، یک پارامتر پیش‌فرض هربار که تابع بدون پارامتر مریوطه صدا زده بشود، ارزیابی می‌شود.

در مثال بالا، anotherFunction() هربار که showMessage() بدون پارامتر text صدا زده بشود، فراخوانی می‌شود.

از سوی دیگر، این به طور مستقل فراخوانی می‌شود وقتی text وجود نداشته باشد.

پارامترهای پیش‌فرض جایگزین

پارامترهای پیش‌فرض در کد جاوااسکریپت قدیمی

چند سال پیش، جاوااسکریپت از سینتکس برای پارامترهای پیش‌فرض پشتیبانی نمی‌کرد. پس افراد از روش‌های دیگر برای تعیین آن‌ها استفاده می‌کردند.

امروزه، در اسکریپت‌های قدیمی می‌توانیم با آن‌ها برخورد داشته باشیم.

برای مثال، یک بررسی ضمنی برای undefined:

function showMessage(from, text) {
  if (text === undefined) {
    text = 'متنی داده نشده';
  }

  alert( from + ": " + text );
}

…یا استفاده از عمگر ||:

function showMessage(from, text) {
  // بود، مقدار پیش‌فرض را تخصیص بده falsy از نوع text اگر مقدار
  // با وجود نداشتن متن یکسان است text == "" این یعنی
  text = text || 'متنی داده نشده';
  ...
}

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

برای بررسی یک پارامتر حذف شده، می‌توانیم آن را با undefined مقایسه کنیم:

function showMessage(text) {
  // ...

  if (text === undefined) { // if the parameter is missing
    text = 'empty message';
  }

  alert(text);
}

showMessage(); // empty message

…یا می‌توانستیم از عملگر || استفاده کنیم:

function showMessage(text) {
  // if text is undefined or otherwise falsy, set it to 'empty'
  text = text || 'empty';
  ...
}

موتورهای جاوااسکریپت مدرن از عملگر nullish coalescing ?? پشتیبانی می‌کنند، این عملگر زمانی که مقدارهای falsy مثل 0 معمولی فرض می‌شوند، بهتر است:

function showCount(count) {
  // if count is undefined or null, show "unknown"
  alert(count ?? "unknown");
}

showCount(0); // 0
showCount(null); // unknown
showCount(); // unknown

بازگردانی یک مقدار (Returning a value)

یک تابع می‌تواند مقداری را در فراخوانی کد به عنوان یک جواب بازگرداند.

ساده‌ترین مثال یک تابعی‌ست که جمع دو عدد را حساب می‌کند:

function sum(a, b) {
  return a + b;
}

let result = sum(1, 2);
alert( result ); // 3

return میتواند در هرجایی از تابع باشد. وقتی اجرای تابع به آن می‌رسد، تابع متوقف می‌شود و مقدار به کد صدازده شده، بازگردانده ‌می‌شود (که در کد بالا result است.)

return ممکن است در یک تابع بارها ظاهر شود. برای مثال:

function checkAge(age) {
  if (age >= 18) {
    return true;
  } else {
    return confirm('Do you have permission from your parents?');
  }
}

let age = prompt('How old are you?', 18);

if ( checkAge(age) ) {
  alert( 'Access granted' );
} else {
  alert( 'Access denied' );
}

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

برای مثال:

function showMovie(age) {
  if ( !checkAge(age) ) {
    return;
  }

  alert( "Showing you the movie" ); // (*)
  // ...
}

در کد بالا، اگر checkAge(age)، false برگرداند، سپس، showMovie به alert نمی‌رسد.

یک تابع با مقدار خالی return یا بدون آن، undefined برمی‌گرداند.

اگر یک تابع مقداری را برنگرداند، مثل این می‌ماند که undefined را برگردانده باشد:

function doNothing() { /* empty */ }

alert( doNothing() === undefined ); // true

مقدار return خالی، مثل return undefined است:

function doNothing() {
  return;
}

alert( doNothing() === undefined ); // true
هرگز خط خالی بین return و مقدار نگذارید

برای جمله‌ای طولانی در return، شاید وسوسه کننده به نظر برسد که در یک خطی جدا بدین شکل بگذاریم:

return
 (some + long + expression + or + whatever * f(a) + f(b))

اما این کار نمی‌کند چون جاوااسکریپت بعد return یک ; فرض می‌گیرد. مثل:

return;
 (some + long + expression + or + whatever * f(a) + f(b))

بنابراین، به یک بازگردانی خالی تبدیل می‌شود.

اگر ما بخواهیم که عبارت برگردانده شده در چندین خط باشد، باید آن را در همان خط return آغاز کنیم. یا حداقل پرانتز اول (باز شونده) را آنجا بگذاریم، مانند کد زیر:

return (
  some + long + expression
  + or +
  whatever * f(a) + f(b)
  )

این کد همانطور که ما توقع داریم کار می‌کند.

نامگذاری یک تابع

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

این یک روش معمول است که تابع را با پیشوند فعلی شروع کنیم که کارش را به گنگی توصیف کند. یک توافقی در تیم باید بر معنی‌های این پیشوندها باشد.

برای نمونه، توابعی که با "show" شروع می‌شوند، معمولا چیزی را نمایش می‌دهند.

توابعی که با این‌ها شروع می‌شوند…

  • "get…" – مقداری را برمی‌گرداند،
  • "calc…" – چیزی را محاسبه می‌کند،
  • "create…" – چیزی را می‌سازد،
  • "check…" – چیزی را بررسی می‌کند و مقدار boolean برمی‌گرداند و غیره.

نمونه‌هایی از چنین نام‌هایی:

showMessage(..)     // پیغامی را نشان می‌دهد.
getAge(..)          // سن را برمی‌گرداند که به نحوی مقدارش به آن رسیده
calcSum(..)         // جمع می‌کند و جواب را برمی‌گرداند
createForm(..)      // یک فرم می‌سازد و عموما آن را برمی‌گرداند
checkPermission(..) // یک سطح دسترسی را بررسی می‌کند و صحیح و غلط برمی‌گرداند

با پیشوند‌ها در جای خود، نگاهی کوتاه به نام تابع، درکی از نوع کار و مقداری که برمی‌گرداند را به ما می‌دهد.

یک تابع، یک فعالیت

یک تابع بایستی چیزی که از نامش پیداست را انجام بدهد، نه بیشتر.

دو فعالیت مستقل،‌ عموما به دو تابع نیاز دارند، حتی اگر عموما باهم صدا زده می‌شوند (در این حالت می‌توانیم یک تابع سومی بسازیم که دوتای دیگر را صدا می‌زند)

مثال هایی از شکستن این قانون:

  • getAge – کار خوبی نیست اگر یک alert را همراه با سن نشان بدهد (فقط باید دریافت کند).
  • createForm – کار خوبی نیست اگر document را تغییر بدهد یا فرمی به آن اضافه کند (باید فقط آنرا بسازد و برگرداند).
  • checkPermission – کار خوبی نیست اگر پیام access granted/denied را نشان دهد (فقط باید بررسی را اجرا کند و مقدار را برگرداند).

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

نام‌های خیلی کوتاه تابع

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

برای مثال، فریمورک jQuery یک تابع را با $ تعریف می‌کند. کتابخانه‌ Lodash هم تابع اصلی‌ش با نام _ است.

اینها استثنا هستند. عموما اسم‌های توابع باید مختصر و توصیفی باشند.

Functions == Comments

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

یک تابع مجزا نه تنها برای آزمودن و Debug کردن ساده‌تر است بلکه حتی وجود داشتنش هم توصیفی از نحوه کارکرد است.

برای نمونه، دو تابع showPrimes(n) زیر را مقاسیه کنید. هر یک اعداد اول را تا n خروجی می‌دهد.

حالت اول از برچسب استفاده می‌کند:

function showPrimes(n) {
  nextPrime: for (let i = 2; i < n; i++) {

    for (let j = 2; j < i; j++) {
      if (i % j == 0) continue nextPrime;
    }

    alert( i ); // a prime
  }
}

در حالت دوم، از یک تابع افزوده‌ای به نام isPrime(n) برای بررسی اول بودن استفاده می‌شود:

function showPrimes(n) {

  for (let i = 2; i < n; i++) {
    if (!isPrime(i)) continue;

    alert(i);  // a prime
  }
}

function isPrime(n) {
  for (let i = 2; i < n; i++) {
    if ( n % i == 0) return false;
  }
  return true;
}

حالت دوم قابل فهم‌تر است، نه؟ تفاوت حالت دوم این است که به جای کد، یک تابع با نام (isPrime) اضافه شده است. بعضی اوقات به اینجور کدها، کدهای خود-توصیف می‌گویند.

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

خلاصه

تعریف یک تابع شبیه این است:

function name(parameters, delimited, by, comma) {
  /* code */
}
  • مقادیر پاس داده شده به یک تابع به عنوان پارامتر، در متغیرهای محلی کپی می‌شوند.
  • یک تابع ممکن است به متغیرهای بیرونی هم دسترسی داشته باشد. اما این موضوع فقط از داخل به بیرون کار می‌کند. کد بیرون از تابع، متغیرهای محلی را نمی‌بیند.
  • یک تابع می‌تواند یک مقدار را برگرداند. در غیر این صورت مقدار undefined را برمی‌گرداند.

برای قابل فهم و تمیز بودن کد، توصیه می‌شود از متغیرهای محلی و پارامترهای تابع را استفاده کنیم تا متغیرهای بیرونی.

فهم یک تابع که پارامترهایی را می‌گیرد و با آن‌ها کار می‌کند و سپس یک خروجی می‌دهد همیشه ساده‌تر است تا یک تابع که هیچ پارامتری نمی‌گیرد اما متغیرهای بیرونی را تغییر می‌دهد.

نامگذاری تابع:

  • یک نام، به طور واضح توضیح می‌دهد که تابع چه کاری انجام می‌دهد. وقتی تابعی در کد صدا زده می‌شود، یک اسم خوب سریعا باعث می‌شود ما متوجه شویم که چه کاری می‌کند و چه چیزی را برمی‌گرداند.
  • یک تابع، یک فعالیت است، بنابراین اسم توابع عموما افعال خطابی هستند.
  • پیشوندهای شناخته‌شده‌ی زیادی برای توابع وجود دارند مانند create، show، get، check... و غیره. از آنها برای اشاره کردن به اینکه تابع چه کاری انجام می‌دهد استفاده کنید.

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

تمارین

اهمیت: 4

تابع زیر true را برمیگرداند اگر پارامتر age از 18 بزرگتر باشد.

در غیر اینصورت برای تأیید سوال می‌پرسد و سپس جواب را بر‌میگرداند:

function checkAge(age) {
if (age > 18) {
return true;
} else {
// ...
return confirm('Did parents allow you?');
}
}

آیا اگر else را حذف کنیم تابع جور دیگری کار می‌کند؟

function checkAge(age) {
if (age > 18) {
return true;
}
// ...
return confirm('Did parents allow you?');
}

آیا هیچ تفاوتی در رفتار این دو حالت وجود دارد؟

فرقی ندارد.

در هر دو مورد، return confirm('Did parents allow you?') دقیقا زمانی که شرط if برابر falsy باشد اجرا می‌شود.

اهمیت: 4

تابع زیر true را برمی‌گرداند اگر پارامتر age از 18 بزرگتر باشد.

در غیر این صورت برای تأیید سوال می‌پرسد و سپس جواب را بر‌می‌گرداند.

function checkAge(age) {
  if (age > 18) {
    return true;
  } else {
    return confirm('Did parents allow you?');
  }
}

کد را بازنویسی کنید، تا همین رفتار، بدون if، در یک خط اجرا شود.

دو حالت از checkAge بسازید:

  1. با استفاده از عملگر علامت سوال ?
  2. با استفاده از OR ||

با استفاده از یک عملگر علامت سوال '?':

function checkAge(age) {
  return (age > 18) ? true : confirm('Did parents allow you?');
}

با استفاده از OR || (کوتاه‌ترین حالت)

function checkAge(age) {
  return (age > 18) || confirm('Did parents allow you?');
}

توجه داشته باشید که پرانتزهای دور age > 18 لازم نیست. برای خوانایی نوشته شده‌ند.

اهمیت: 1

تابع min(a,b) را بنویسید که کمترین را از دو عدد a و b خروجی می‌دهد.

برای نمونه:

min(2, 5) == 2
min(3, -1) == -1
min(1, 1) == 1

راه حل با کمک if:

function min(a, b) {
  if (a < b) {
    return a;
  } else {
    return b;
  }
}

راه حل با علامت سوال '?':

function min(a, b) {
  return a < b ? a : b;
}

پی‌نوشت: در حالت a == b، مهم نیست که چه چیزی برگردانده شود.

اهمیت: 4

تابع pow(x,n) را بنویسید که x به توان n را برمی‌گرداند. یا به عبارتی دیگر، x را n بار در خود ضرب می‌کند و حاصل را برمی‌گرداند.

pow(3, 2) = 3 * 3 = 9
pow(3, 3) = 3 * 3 * 3 = 27
pow(1, 100) = 1 * 1 * ...* 1 = 1

یک صفحه وب که دو مقدار x و n را prompt می‌کند و جواب pow(x,n) را بر‌می‌گرداند بسازید.

[نمونه]

پی‌نوشت: در این تکلیف تابع باید فقط مقدارهای طبیعی n را پوشش دهد: اعداد صحیح از 1 به بعد.

function pow(x, n) {
  let result = x;

  for (let i = 1; i < n; i++) {
    result *= x;
  }

  return result;
}

let x = prompt("x?", '');
let n = prompt("n?", '');

if (n < 1) {
  alert(`Power ${n} is not supported, use a positive integer`);
} else {
  alert( pow(x, n) );
}
نقشه آموزش