Chapter 3توابع

People think that computer science is the art of geniuses but the actual reality is the opposite, just many people doing things that build on each other, like a wall of mini stones.

Donald Knuth
Picture of fern leaves with a fractal shape

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

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

معمولا، انگلیسی زبانان بزرگسال در دامنه واژگانشان حدود 20,000 کلمه دارند. زبان‌های برنامه نویسی کمی هستند که 20,000 دستور از پیش تعریف شده دارند. و این دستورات یا واژگانی که در دسترس هستند با دقت تعریف می شوند که در نتیجه انعطاف کمتری نسبت به زبان‌های بشری دارند. بنابراین، معمولا لازم است مفاهیم جدیدی را برای اجتناب از تکرار بی‌مورد تعریف کنیم.

تعریف یک تابع

تعریف یک تابع همان تعریف عادی یک انتساب (متغیر) است با این تفاوت که مقداری که به متغیر اختصاص داده می شود، از جنس تابع است. به عنوان مثال، کد پیش رو متغیر square را تعریف می کند که به تابعی ارجاع می دهد که مربع عدد داده شده را تولید می کند:

const square = function(x) {
  return x * x;
};

console.log(square(12));
// → 144

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

یک تابع می تواند چندین پارامتر داشته باشد یا هیچ پارامتری نداشته باشد. در مثال پیش رو، تابع makeNoise هیچ پارامتری ندارد در حالیکه تابع power دو پارامتر دارد:

const makeNoise = function() {
  console.log("Pling!");
};

makeNoise();
// → Pling!

const power = function(base, exponent) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};

console.log(power(2, 10));
// → 1024

بعضی توابع مقداری را برمی گردانند،‌ مانند تابع power و square، و بعضی توابع مانند makeNoise فقط اثر جانبی تولید می کنند و مقداری را باز نمی گردانند. دستوری به نام return مسئول بازگرداندن مقداری از تابع است. زمانی که برنامه به این دستور می رسد، به سرعت از تابع فعلی خارج شده و مقداری که “برگردانده شده” را به قسمتی از برنامه که تابع در آنجا فراخوانی شده، ارسال می کند. استفاده از دستور return بدون عبارتی بعد از آن ، باعث می شود که تابع مقدار undefined را برگرداند. توابعی که اصلا دستور return را ندارند مانند makeNoise نیز مقدار undefined را برمی گردانند.

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

متغیرها و قلمرو‌ی آن - Scope

هر متغیری دارای یک قلمروی (scope) دسترسی است که عبارت است از بخشی از برنامه که در آن، متغیر قابل دسترس و مشاهده است. برای متغیرهایی که بیرون از یک تابع یا یک بلاک تعریف شده اند، این حوزه شامل کل برنامه می شود – می توانید به این متغیرها در هرجای برنامه دسترسی داشته باشید. به این متغیرها متغیرهای سراسری گفته می شود.

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

متغیرهایی که با let یا const تعریف می شوند در واقع نسبت به بلاکی که درون آن تعریف شده اند محلی هستند (متعلق به همان بلاک می باشند)، بنابراین اگر یکی از آن‌ها را درون یک حلقه تعریف کنید، کدی که قبل و بعد از حلقه قرار گرفته نمی تواند آن را “ببیند”. در نسخه‌های قبل از 2015 جاوااسکریپت، فقط توابع بودند که می توانستند قلمرو یا حوزه‌ی جدیدی ایجاد کنند، بنابراین متغیرهای قدیمی که با var تعریف می شدند، در تابعی که در آن تعریف شده بودند، دیده می شدند. اگر هم درون یک تابع نبودند، در سراسر برنامه در دسترس بودند.

let x = 10;
if (true) {
  let y = 20;
  var z = 30;
  console.log(x + y + z);
  // → 60
}
// y is not visible here
console.log(x + z);
// → 40

هر قلمرو می تواند قلمروی پیرامونش را ببیند، مثلا در برنامه‌ی بالا، متغیر x در درون بلاک قابل مشاهده است. یک استثناء اینجا زمانی رخ می دهد که چندین متغیر با یک نام وجود داشته باشد که در این صورت، کد برنامه می تواند درونی‌ترین مورد را ببیند. به عنوان مثال، در تابع halve مثال پایین، زمانی که کد درون تابع به متغیر n اشاره می کند، متغیر n خود تابع استفاده می شود نه متغیر سراسری n.

const halve = function(n) {
  return n / 2;
};

let n = 10;
console.log(halve(100));
// → 50
console.log(n);
// → 10

قلمروی تو در تو

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

به عنوان مثال، این تابع – که مواد لازم برای تهیه حمص (نوعی غذا) را برمی گرداند – تابعی دیگر در درون خود دارد:

const hummus = function(factor) {
  const ingredient = function(amount, unit, name) {
    let ingredientAmount = amount * factor;
    if (ingredientAmount > 1) {
      unit += "s";
    }
    console.log(`${ingredientAmount} ${unit} ${name}`);
  };
  ingredient(1, "can", "chickpeas");
  ingredient(0.25, "cup", "tahini");
  ingredient(0.25, "cup", "lemon juice");
  ingredient(1, "clove", "garlic");
  ingredient(2, "tablespoon", "olive oil");
  ingredient(0.5, "teaspoon", "cumin");
};

کدی که در تابع ingredient قرار دارد می تواند متغیر factor را از تابع بیرونی ببیند. اما متغیرهای محلیش مانند unit یا ingredientAmount توسط تابع بیرونی قابل مشاهده نیستند.

مجموعه متغیرهایی که درون یک بلاک قابل رویت هستند بستگی به مکان بلاک در متن برنامه دارند. هر قلمروی محلی همچنین می تواند قلمرو‌های محلی ای که آن را در بر گرفته اند را ببیند و همه قلمروها می تواند قلمرو سراسری را ببینند. این روش رویت پذیری متغیر را حوزه‌بندی لغوی (Lexical Scoping) می نامند.

استفاده از توابع به عنوان مقدار

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

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

let launchMissiles = function() {
  missileSystem.launch("now");
};
if (safeMode) {
  launchMissiles = function() {/* do nothing */};
}

در فصل 5، در مورد کارهای جالبی که می توان به وسیله ارسال توابع به عنوان مقدار به دیگر توابع انجام داد بحث خواهیم کرد.

استفاده از روش اعلان تابع

روش کمی خلاصه‌تر برای تعریف یک تابع وجود دارد. اگر در ابتدای یک دستور (statement)، کلمه‌ی کلیدی function استفاده شود، معنای دیگری خواهد داشت.

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

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

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

console.log("The future says:", future());

function future() {
  return "You'll never have flying cars";
}

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

Arrow functions - توابع پیکانی

روش سومی هم برای تعریف توابع وجود دارد، که با روش‌های قبلی خیلی متفاوت‌ به نظر می رسد. به جای استفاده از کلیدواژه‌ی function از یک پیکان (=>) که از علامت مساوی و بزرگتر تشکیل شده است استفاده می شود. ( نباید با عملگر بزرگتر-یا-مساوی-از اشتباه گرفته شود، که به شکل >= نوشته می شود)

const power = (base, exponent) => {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};

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

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

const square1 = (x) => { return x * x; };
const square2 = x => x * x;

زمانی که یک arrow function فاقد پارامتر است، لیست پارامترهایش به صورت یک جفت پرانتز خالی نوشته می شود.

const horn = () => {
  console.log("Toot");
};

نمی توان دلیل عمیقی برای وجود نیاز به هر دو نوع تعریف تابع (روش پیکانی و معمول) ، پیدا کرد. از موارد جزئی که بگذریم ( که در فصل 6 به آن ها می پردازیم)، هر دوی آن ها کار یکسانی را انجام می دهند. روش پیکانی در نسخه‌ی 2015 به زبان اضافه شد بیشتر به این خاطر که بتوان توابع را به شکل کوتاه‌تر و خلاصه نوشت و از درازنویسی پرهیز کرد. در فصل 5 از آن‌ها زیاد استفاده خواهیم کرد.

پشته‌ی فراخوانی توابع

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

function greet(who) {
  console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

اجرای این برنامه به طور کلی به این شکل خواهد بود: فراخوانی تابع greet باعث می شود که کنترل برنامه به شروع بدنه‌ی آن تابع (خط 2) بپرد. تابع console.log (که تابعی از پیش ساخته شده در مرورگر است) را فراخوانی می کند، که کنترل را در دست گرفته، کارش را انجام می دهد و دوباره کنترل را به خط 2 بازمی گرداند. سپس به انتهای تابع greet می رسد بنابراین به جایی که در ابتدا فراخوانی شده بود باز می گردد، خط 4. خط بعدی دوباره console.log را فراخوانی می کند. بعد از آن، برنامه به پایان خود می رسد.

می توانیم این جریان کنترل را به صورت شماتیک به این صورت نشان دهیم:

not in function
   in greet
        in console.log
   in greet
not in function
   in console.log
not in function

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

جایی از کامپیوتر که این محل یا زمینه (context) ذخیره می شود پشته‌ی فراخوانی می باشد. هر بار که تابعی فراخوانی می شود، محل فعلی فراخوانی در بالای این “پشته” قرار می گیرد. زمانی که اجرای تابع تمام می شود، عنصر بالای پشته را از پشته حذف می کند و از آن محل برای ادامه اجرای برنامه استفاده می کند.

ذخیره‌ی این پشته نیاز به فضایی در حافظه‌ی کامپیوتر دارد. در صورت رشد بیش از اندازه پشته، کامپیوتر با مشکل روبرو شده و پیغام “out of stack space” یا “too much recursion” را تولید می کند. کدی که در ادامه می آید این موضوع را بیشتر باز می کند. در این مثال کامپیوتر با مسئله‌ی بسیار مشکلی روبرو می شود که موجب می شود به طور بی نهایت بین دو تابع گیر بیفتد. اگر محدودیت حافظه‌ برای پشته نبود، احتمالا اجرای این برنامه بی نهایت می شد. اما در واقعیت، ما با کمبود فضا روبرو می شویم، یا اینکه پشته از کار خواهد افتاد.

function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
console.log(chicken() + " came first.");
// → ??

آرگومان‌های اختیاری

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

function square(x) { return x * x; }
console.log(square(4, true, "hedgehog"));
// → 16

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

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

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

و جنبه مثبت این است که می توان با استفاده از آن به یک تابع اجازه داد که آرگومان‌های متعدد و متفاوتی فراخوانی شود. به عنوان مثال، تابع minus که در ادامه می آید سعی می کند که عملگر - را با عمل کردن روی یک یا دو آرگومان شبیه سازی کند:

function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}

console.log(minus(10));
// → -10
console.log(minus(10, 5));
// → 5

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

به عنوان مثال، این نسخه از تابع power آرگومان دومش را اختیاری تعریف کرده است. اگر آرگومان دوم را ارسال نکنید یا اینکه مقدار undefined را بفرستید، مقدار پیش‌فرض 2 در نظر گرفته می شود و تابع شبیه تابع square عمل می کند.

function power(base, exponent = 2) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
}

console.log(power(4));
// → 16
console.log(power(2, 6));
// → 64

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

console.log("C", "O", 2);
// → C O 2

بستار (closure)

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

کدی که در ادامه می آید مثالی از این مفهوم را نمایش می دهد. تابعی به نام wrapValue را تعریف کرده که درون آن متغیری محلی ایجاد شده است. سپس تابعی را بازمیگرداند که به این متغیر محلی دسترسی داشته و آن را برمی گرداند.

function wrapValue(n) {
  let local = n;
  return () => local;
}

let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

این کار مجاز و معتبر است و به خوبی کار می کند درست همانطور که انتظارش را دارید – هر دوی نمونه‌های متغیر مورد نظر هنوز قابل دستیابی هستند. این وضعیت مثال خوبی از این واقعیت است که متغیرهای محلی با هر با فراخوانی از نو ایجاد می شوند و فراخوانی های مختلف به متغیرهای محلی هر فراخوانی آسیبی نمی رساند.

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

با کمی تغییر می توانیم مثال قبل را به روشی برای ایجاد توابعی که عمل ضرب را با یک مقدار دلخواه انجام می دهند استفاده کنیم.

function multiplier(factor) {
  return number => number * factor;
}

let twice = multiplier(2);
console.log(twice(5));
// → 10

نیازی نیست مانند مثال wrapValue به طور صریح متغیر local وجود داشته باشد چون پارامتر یک تابع خود یک متغیر محلی محسوب می شود.

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

در مثال، multiplier فراخوانی شده و محیطی را ایجاد کرده است که در آن پارامتر factor به 2 اختصاص داده شده است. مقدار تابعی که برمی گرداند، که در twice ذخیره شده است، محیط را به خاطر می آورد. بنابراین در هنگام فراخوانی، آرگومانش را در 2 ضرب می کند.

بازگشتی

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

function power(base, exponent) {
  if (exponent == 0) {
    return 1;
  } else {
    return base * power(base, exponent - 1);
  }
}

console.log(power(2, 3));
// → 8

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

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

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

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

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

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

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

به این مسئله توجه کنید: با شروع از عدد 1 و اضافه‌ کردن عدد 5 یا ضرب در عدد 3 به صورت مداوم، بی نهایت عدد جدید می توان تولید کرد. چگونه می توانید تابعی بنویسید که عددی را گرفته و دنباله‌ای از ضرب و جمع‌هایی که منجر به تولید آن عدد شده است را برگرداند؟

به عنوان مثال، عدد 13 را می توان با ضرب 1 در 3 و دو بار افزودن 5 بدست آورد در حالیکه عددی مثل 15 را اصلا نمی توان به این شیوه تولید کرد.

راه حل استفاده از روش بازگشتی:

function findSolution(target) {
  function find(current, history) {
    if (current == target) {
      return history;
    } else if (current > target) {
      return null;
    } else {
      return find(current + 5, `(${history} + 5)`) ||
             find(current * 3, `(${history} * 3)`);
    }
  }
  return find(1, "1");
}

console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)

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

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

تابع درونی find عمل اصلی بازگشتی را انجام می دهد. دو آرگومان می گیرد که شامل عدد فعلی و رشته‌ای است که نحوه رسیدن به عدد را ضبط می کند. اگر راه حلی پیدا کرد ، رشته‌ای حاوی دنباله‌ی عملیات تا عدد مورد نظر را بر می گرداند. اگر راه حلی وجود نداشت، مقدار null را بر می گرداند.

برای این کار، تابع یکی از این سه کار را انجام می دهد. اگر عدد فعلی عدد هدف بود، تاریخچه‌ی فعلی (history) به عنوان پاسخ برگردانده می شود. اگر عدد فعلی از عدد هدف بزرگتر بود، معنایی ندارد که کاوش بیشتری برای کشف تاریخچه انجام شود زیرا هر عمل جمع یا ضرب عدد را فقط بزرگتر می کند بنابراین مقدار null را برمی‌گرداند. در نهایت، اگر هنوز عدد فعلی از عدد هدف کوچکتر باشد، تابع هر دو مسیر ممکن که از عدد فعلی شروع می شود را آزمایش می کند و این کار را با دوبار فراخوانی خودش، یکبار برای جمع و یکبار برای ضرب انجام می دهد. اگر اولین فراخوانی چیزی به غیر از null را تولید کرد، آن را بر می گرداند. در غیر این صورت، فراخوانی دوم بازگردانده می شود – فارغ از اینکه رشته یا null را تولید کند.

برای درک بهتر نحوه عملکرد تابع اجازه دهید به همه‌ی فراخوانی‌های find که برای پیدا کردن جواب مساله برای عدد 13 اتفاق می افتد نگاهی بیاندازیم.

find(1, "1")
  find(6, "(1 + 5)")
    find(11, "((1 + 5) + 5)")
      find(16, "(((1 + 5) + 5) + 5)")
        too big
      find(33, "(((1 + 5) + 5) * 3)")
        too big
    find(18, "((1 + 5) * 3)")
      too big
  find(3, "(1 * 3)")
    find(8, "((1 * 3) + 5)")
      find(13, "(((1 * 3) + 5) + 5)")
        found!

تورفتگی موجود در کد بالا برای نشان دادن عمق پشته‌ی فراخوانی توابع است. اولین بار که find فراخوانی می شود، خودش را برای کاوش راه حلی که با (1 + 5) شروع می شود فراخوانی می کند. این فراخوانی با استفاده بازگشت، تمامی راه حل هایی که عددی کمتر یا برابر عدد هدف را تولید می کند را مورد کاوش قرار می دهد. با توجه به این که این فراخوانی نمی تواند به راه حلی برسد مقدار null به عنوان خروجی فراخوانی اول بازگردانده می شود. عملگر || در اینجا باعث کاوش (1 * 3) می شود. این جستجو شانس بیشتری دارد — اولین فراخوانی بازگشتی آن، توسط فراخوانی بازگشتی‌اش، به عدد هدف می رسد . درونی ترین فراخوانی بازگشتی، یک رشته را بر می گرداند و هر کدام از عملگرهای || در فراخوانی‌های میانی، آن رشته را دست به دست می کنند تا در نهایت راه حل برگردانده شود.

رشد توابع

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

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

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

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

قصد داریم تا برنامه‌ای بنویسیم که دو عدد را چاپ کند: تعداد گاو‌ها و مرغ‌های یک مزرعه به همراه کلمات Cows و Chickens بعد از آن ها و نشان دادن هر دوی عددها با طول 3 رقم ( استفاده از 0 برای ترازبندی)

007 Cows
011 Chickens

با توجه به مسئله به تابعی با دو آرگومان نیاز داریم. تعداد گاو‌ها و تعداد مرغ‌ها.

function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = "0" + cowString;
  }
  console.log(`${cowString} Cows`);
  let chickenString = String(chickens);
  while (chickenString.length < 3) {
    chickenString = "0" + chickenString;
  }
  console.log(`${chickenString} Chickens`);
}
printFarmInventory(7, 11);

استفاده از .length بعد از مقدار رشته‌ای، طول رشته را به ما می دهد. بنابراین، حلقه while عمل افزودن صفر به ابتدای رشته‌ی اعداد را تا زمانی که حداقل طول آن سه کاراکتر بشود ادامه می دهد.

ماموریت تمام است! اما درست زمانی که قرار است برنامه را برای کشاورز مثال‌مان ( به همراه صورت حساب) ارسال کنیم، با ما تماس می گیرد و اعلام می کند که اخیرا پرورش خوک را نیز شروع کرده است و آیا ما می توانیم قابلیت نرم افزار را افزایش داده تا تعداد خوک ها را نیز چاپ کند؟

حتما می توانیم. اما درست وقتی که یک بار دیگر در حال کپی و الصاق آن چهار خط هستیم، کمی صبر کرده و تجدید نظر می کنیم. باید راه بهتری برای این کار وجود داشته باشد. اولین تلاش ما اینگونه است:

function printZeroPaddedWithLabel(number, label) {
  let numberString = String(number);
  while (numberString.length < 3) {
    numberString = "0" + numberString;
  }
  console.log(`${numberString} ${label}`);
}

function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, "Cows");
  printZeroPaddedWithLabel(chickens, "Chickens");
  printZeroPaddedWithLabel(pigs, "Pigs");
}

printFarmInventory(7, 11, 3);

روش جدید به خوبی کار می کند. اما نام printZeroPaddedWithLabel کمی ناجور به نظر می رسد. ظاهرا سه چیز مختلف – چاپ کردن (printing)، ترازکردن با صفر(zero-padding) و افزودن برچسب (adding a lable) – در یک تابع مخلوط شده است.

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

function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = "0" + string;
  }
  return string;
}

function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Cows`);
  console.log(`${zeroPad(chickens, 3)} Chickens`);
  console.log(`${zeroPad(pigs, 3)} Pigs`);
}

printFarmInventory(7, 16, 3);

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

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

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

توابع و اثرات جانبی

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

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

یک تابع ناب (pure) شکل خاصی از یک تابعی است که مقداری را بر می گرداند و نه تنها خودش اثر جانبی ندارد، بلکه به اثرات جانبی دیگر کدها نیز وابستگی ندارد – مثلا متغیرهای سراسری که ممکن است در کدهای دیگر تغییر کنند را مورد استفاده قرار نمی دهد. یک تابع ناب ویژگی خوبش در این است که اگر با آرگومان‌های ثابت و مشابهی فراخوانی شود، همیشه مقدار مشابهی را برمیگرداند (و رفتار متفاوتی انجام نمی دهد). فراخوانی تابعی با این ویژگی را می توان بدون تغییر در معنای کد برنامه، معادل مقدار بازگشتی‌اش در نظر گرفت. زمانی که از صحت عملکرد یک تابع ناب مطمئن نیستید به راحتی می توانید با فراخوانی، آن را تست کنید و اگر در آن بستر (context) به درستی کار کرد، در همه‌ی بسترها هم به درستی کار خواهد کرد. توابع غیرناب اما برای آزمایش نیاز به شرایط و پیش‌نیازهای بیشتری دارند.

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

خلاصه

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

// Define f to hold a function value
const f = function(a) {
  console.log(a + 2);
};

// Declare g to be a function
function g(a, b) {
  return a * b * 3.5;
}

// A less verbose function value
let h = a => a % 3;

نقطه‌ی کلیدی در فهم توابع، درک مفهوم قلمرو‌ها است. هر بلاک از کد قلمروی جدیدی را ایجاد می کند. پارامترها و متغیرهایی که درون یک بلاک اعلان می شوند نسبت به آن تابع محلی هستند، و از بیرون بلاک قابل دسترسی نیستند. متغیرهایی که با کلیدواژه‌ی var ایجاد می شوند به شکل متفاوتی عمل می کنند – محدوده‌ی آن‌ها تا پایان حوزه‌ی نزدیک ترین تابع یا فضای سراسری برنامه است.

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

تمرین‌ها

کمینه

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

// Your code here.

console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10

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

یک تابع می تواند چندین دستور return داشته باشد.

بازگشت

قبلا دیده بودایم که عملگر % (باقی مانده) را می توان برای تشخیص زوج یا فرد بودن عدد استفاده کرد که برای این کار با استفاده از % 2، بخش پذیری بر 2 مورد آزمایش قرار می گرفت. در اینجا با راهی دیگر برای تشخیص زوج یا فرد بودن یک عدد صحیح مثبت آشنا می شویم:

تابع بازگشتی isEven را با توجه به توضیحات بالا تعریف کنید. تابع باید پارامتری مثبت و از جنس اعداد صحیح دریافت کند و مقداری از جنس بولی برگرداند.

تابع را با مقادیر 50 و 75 تست کنید. بررسی کنید که اگر -1 را به آن بدهید چه خواهد شد. چرا؟ آیا می توانید راهی برای حل مشکل پیش آمده پیدا کنید؟

// Your code here.

console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??

تابع شما احتمالا به چیزی شبیه به تابع درونی find در مثال بازگشتی findSolution نزدیک خواهد بود، که حاوی یک مجموعه‌ی if/else if/else بود که صحت یکی از سه حالت را آزمایش می کرد. else آخر، که به سومین حالت مربوط می شود، عمل فراخوانی بازگشتی را انجام می دهد. هر یک از شاخه‌ها باید یک دستور return داشته باشند یا به شکلی مقداری را برای خروجی مهیا کنند.

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

شمارش دانه

برای بدست آوردن کاراکتر یا حرف Nام یک رشته، می توانید از "string"[N] استفاده کنید. مقداری که برگردانده می شود رشته‌ای است که فقط یک کاراکتر دارد. (مثلا رشته‌ی "b"). کاراکتر اول در جایگاه صفرم قرار دارد. بنابراین آخرین کاراکتر در جایگاه string.length - 1 قرار می گیرد. به عبارتی دیگر، یک رشته‌ی دو کاراکتری طولش 2 کاراکتر است و کاراکتر‌هایش در جایگاه 0 و 1 قرار دارند.

تابعی به نام countBs بنویسید که رشته ای را به عنوان تنها آرگومانش می پذیرد و عددی را برمیگرداند که نشان می دهد چند کاراکتر “B” بزرگ در رشته وجود دارد.

بعد، تابعی به نام countChar بنویسید که شبیه countBs کار می کند اما آرگومان دومی نیز دریافت می کند که مشخص کننده کاراکتری است که بایستی شمرده بشود ( به جای اینکه فقط B شمرده شود ). تابع countBs را بازنویسی کنید تا این ویژگی جدید را داشته باشد.

// Your code here.

console.log(countBs("BBC"));
// → 2
console.log(countChar("kakkerlak", "k"));
// → 4

تابع شما به یک حلقه نیاز دارد که به تک تک کاراکترهای رشته بپردازد. این حلقه می تواند از شاخص صفر تا یک عدد کمتر از طول رشته را پیمایش کند (< string.length). اگر کاراکتر موجود در موقعیت فعلی معادل کاراکتری بود که تابع به دنبال آن است، به متغیر شمارنده 1 واحد اضافه می کند. بعد از پایان حلقه، شمارنده را می توان بازگرداند.

حواستان باشد که همه‌ی متغیرهایی که در تابع استفاده می شود به صورت محلی در همان تابع به وسیله‌ی let یا const تعریف شوند.