۲۵ ژوئیه ۲۰۲۲

شیء تابع، NFE

همانطور که می‌دانیم، یک تابع در جاوااسکریپت یک مقدار است.

هر مقداری در جاوااسکریپت نوع دارد. تابع از چه نوعی است؟

در جاوااسکریپت، تابع‌ها شیء هستند.

یک راه خوب برای تصور کردن تابع‌ها، فکر کردن به آنها به عنوان «شیءهای عملکردی» قابل فراخوانی است. ما نه تنها می‌توانیم آنها را فرا بخوانیم بلکه می‌توانیم با آنها مانند شیءها رفتار کنیم: ویژگی‌ها را اضافه/حذف کنیم، آنها را توسط مرجع رد و بدل کنیم و غیره.

ویژگی “name”

شیء تابع‌ها چند ویژگی قابل استفاده دارند.

برای مثال، اسم یک تابع با ویژگی “name” قابل دسترس است:

function sayHi() {
  alert("Hi");
}

alert(sayHi.name); // sayHi

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

let sayHi = function() {
  alert("Hi");
};

alert(sayHi.name); // sayHi (!یک اسم دارد)

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

function f(sayHi = function() {}) {
  alert(sayHi.name); // sayHi (!کار می‌کند)
}

f();

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

متدهای شیءها هم اسم دارند:

let user = {

  sayHi() {
    // ...
  },

  sayBye: function() {
    // ...
  }

}

alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye

اگرچه هیچ جادویی وجود ندارد. مواردی وجود دارند که راهی برای فهمیدن اسم درست وجود ندارد. در این صورت، ویژگی اسم (name) خالی است، مثل اینجا:

// تابع درون آرایه ساخته شده است
let arr = [function() {}];

alert( arr[0].name ); // <رشته خالی>
// موتور راهی برای دریافت اسم درست ندارد، پس هیچی وجود ندارد

اگرچه در عمل، اکثر تابع‌ها اسم دارند.

ویژگی “length”

یک ویژگی درون‌ساخت دیگر به نام “length” وجود دارد که تعداد پارامترهای تابع را برمی‌گرداند، برای مثال:

function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2

اینجا می‌بینیم که پارامترهای رِست شمرده نمی‌شوند.

ویژگی length بعضی اوقات برای درون‌نگری در تابع‌هایی که بر روی تابع‌های دیگر کاری انجام می‌دهند استفاده می‌شود.

برای مثال، در کد زیر تابع ask یک question (سوال) برای پرسیدن و تعدادی تابع handler (کنترل‌کننده) برای فراخوانی دریافت می‌کند.

زمانی که کاربر جواب خود را وارد کرد، تابع کنترل‌کننده‌ها را فراخوانی می‌کند. ما می‌توانیم دو نوع کنترل‌کننده را رد کنیم:

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

برای اینکه handler را به درستی فراخوانی کنیم، ویژگی handler.length را بررسی می‌کنیم.

ایده این است که ما یک سینتکس کنترل‌کننده ساده و بدون آرگومان برای موارد مثبت داریم (نوعی که بیشتر اتفاق می‌افتد) اما می‌توانیم کنترل‌کننده‌های کلی را هم پوشش دهیم:

function ask(question, ...handlers) {
  let isYes = confirm(question);

  for(let handler of handlers) {
    if (handler.length == 0) {
      if (isYes) handler();
    } else {
      handler(isYes);
    }
  }

}

// برای جواب مثبت، هر دو کنترل‌کننده فراخوانی می‌شوند
// برای جواب منفی، فقط دومی
ask("سوال؟", () => alert('شما بله گفتید'), result => alert(result));

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

ویژگی‌های سفارشی

ما می‌توانیم ویژگی‌هایی از خودمان را هم اضافه کنیم.

اینجا می‌توانیم ویژگی counter را اضافه کنیم تا تعداد تمام فراخوانی‌ها را پیگیری کنیم:

function sayHi() {
  alert("سلام");

  // بیایید تعداد اجرا کردن را بشماریم
  sayHi.counter++;
}
sayHi.counter = 0; // مقدار اولیه

sayHi(); // سلام
sayHi(); // سلام

alert( `${sayHi.counter} بار فراخوانی شد` ); // دو بار فراخوانی شد
ویژگی متغیر نیست

یک ویژگی که به یک تابع تخصیص داده شود مانند sayHi.counter = 0، متغیر محلی counter را درون آن تعریف نمی‌کند. به عبارتی دیگر، یک ویژگی counter و متغیر let counter دو چیز غیر مرتبط هستند.

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

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

function makeCounter() {
  // :به جای این
  // let count = 0

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1

count حالا در به صورت مستقیم در خود تابع ذخیره شده است نه در محیط لغوی بیرونی آن.

این روشِ استفاده از کلوژر بهتر است یا بدتر؟

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

function makeCounter() {

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

counter.count = 10;
alert( counter() ); // 10

پس انتخاب نحوه پیاده‌سازی به اهداف ما بستگی دارد.

Function Expression نام‌گذاری شده

Function Expression نام‌گذاری شده، یا NFE، یک عبارت برای Function Expressionهایی است که یک اسم دارند.

برای مثال، بیایید یک Function Expression معمولی را فرض کنیم:

let sayHi = function(who) {
  alert(`${who} سلام،`);
};

و یک اسم به آن بدهیم:

let sayHi = function func(who) {
  alert(`سلام، ${who}`);
};

آیا ما اینجا چیزی بدست آوردیم؟ هدف اسم اضافی "func" چیست؟

در ابتدا بیایید این را در نظر بگیریم که ما هنوز هم یک Function Expression داریم. اضافه کردن اسم "func" بعد از function آن را تبدیل به Function Declaration نکرد چون هنوز هم به عنوان بخشی از یک مقداردهی ساخته شده است.

اضافه کردن چنین اسمی چیزی را خراب نکرد.

تابع هنوز هم با sayHi() قابل دسترس است:

let sayHi = function func(who) {
  alert(`سلام، ${who}`);
};

sayHi("John"); // John ،سلام

دو چیز خاص درباره اسم func وجود دارد که دلیل‌هایی برای آن داریم:

  1. این اسم به تابع اجازه می‌دهد که به صورت درونی به خودش رجوع کند.
  2. این اسم بیرون از تابع قابل رویت نیست.

برای مثال، تابع sayHi پایین اگر هیچ مقداری برای who تعیین نشود، خودش را با "Guest" صدا می‌زند:

let sayHi = function func(who) {
  if (who) {
    alert(`سلام، ${who}`);
  } else {
    func("Guest"); // از تابع برای اینکه خودش را دوباره صدا بزند استفاده کنید
  }
};

sayHi(); // Guest ،سلام

// :اما این کار نخواهد کرد
func(); // تعریف نشده است (بیرون از تابع قابل رویت نیست) func ،ارور

چرا ما از func استفاده می‌کنیم؟ شاید فقط از sayHi برای فراخوانی تودرتو باید استفاده کنیم؟

در واقع، در اکثر موارد ما می‌توانیم این کار را انجام دهیم:

let sayHi = function(who) {
  if (who) {
    alert(`سلام، ${who}`);
  } else {
    sayHi("Guest");
  }
};

مشکل این کد، امکان تغییر sayHi در کد بیرونی است. اگر تابع به یک متغیر دیگر تخصیص داده شود، کد شروع به ایجاد ارور می‌کند:

let sayHi = function(who) {
  if (who) {
    alert(`سلام، ${who}`);
  } else {
    sayHi("Guest"); // تابع نیست sayHi :ارور
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // !دیگر کار نمی‌کند sayHi ارور، فراخوانی تودرتوی

دلیل بروز ارور این است که تابع sayHi را از محیط لغوی بیرونی دریافت می‌کند. هیچ sayHi محلی وجود ندارد پس متغیر بیرونی استفاده می‌شود. و در زمان فراخوانی sayHi بیرونی برابر با null است.

اسم اختیاری که ما می‌توانیم در Function Expression قرار می‌دهیم قرار است که دقیقا این دسته از مشکلات را حل کند.

بیایید از آن برای رفع مشکل کد خود استفاده کنیم:

let sayHi = function func(who) {
  if (who) {
    alert(`سلام، ${who}`);
  } else {
    func("Guest"); // حالا همه چیز درست است
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // (فراخوانی تودرتو کار می‌کند) Guest ،سلام

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

کد بیرونی هنوز هم متغیر sayHi یا welcome خود را دارد. و func یک «اسم درونی تابع» است، جوری که تابع می‌توانند از درون خودش را فراخوانی کند.

چنین چیزی برای Function Declaration وجود ندارد

خصوصیت «اسم درونی» که اینجا توضیح داده شد فقط برای Function Expessionها قابل استفاده است نه برای Function Declarationها. برای Function Declarationها، سینتکسی برای اضاف کردن اسم «درونی» وجود ندارد.

بعضی‌اوقات، نیاز به یک اسم درونی قابل، دلیلی برای نوشتن دوباره‌ی یک Function Declaration به Function Expression نام‌گذاری‌شده است.

خلاصه

تابع‌ها شیء هستند.

اینجا ما ویژگی‌های آنها را پوشش دادیم:

  • name – اسم تابع. نه تنها در تعریف یک تابع وجود دارد، بلکه در مقداردهی‌ها و ویژگی‌های شیء هم موجود است.
  • length – تعداد آرگومان‌ها در تعریف تابع. پارامترهای رست شمرده نمی‌شوند.

اگر تابعی به عنوان Function Expression تعریف شود (نه در جریان اصلی کد)، و اسمی داشته باشد، سپس به آن یک Function Expression نام‌گذاری شده می‌گویند. اسم می‌تواند درون آن برای رجوع به خودش استفاده شود، مثلا برای فراخوانی‌های بازگشتی یا چنین چیزی.

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

آنها یک تابع «اصلی» می‌سازند و تعداد زیادی از تابع‍‌های «کمکی» را به آن متصل می‌کنند. برای مثال، کتابخانه jQuery یک تابع به نام $ می‌سازد. کتابخانه lodash یک تابع _ می‌سازد و سپس ویژگی‌های _.clone، _keyBy و بقیه ویژگی‌ها را به آن اضافه می‌کند (زمانی که می‌خواهید درباره آنها بیشتر بدانید، مستندات را ببینید). در واقع، آنها این کار را برای کاهش آلودگی فضای گلوبال انجام می‌دهند تا یک کتابخانه فقط یک متغیر گلوبال داشته باشد. این باعث کاهش احتمال وقوع تناقض در نام‌گذاری می‌شود.

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

تمارین

اهمیت: 5

کد makeCounter() را طوری تغییر دهید که شمارنده بتواند هم عدد را تنظیم کند و هم آن را کاهش دهد:

  • counter() باید عدد بعدی را برگرداند (مانند قبل).
  • counter.set(value) باید شمارنده را در value تنظیم کند.
  • counter.decrease() باید از شمارنده به اندازه 1 کم کند.

برای دیدن مثالی کامل از نحوه استفاده، کد جعبهٔ شنی(sandbox) را ببینید.

پی‌نوشت: شما می‌توانید از کلوژر یا ویژگی تابع برای حفظ کردن شماره کنونی استفاده کنید. یا هر دو نوع را بنویسید.

باز کردن یک sandbox همراه با تست‌ها.

راه‌حل از count در متغیر محلی استفاده می‌کند اما متدهای اضافی درست درون counter نوشته شده‌اند. آنها محیط لغوی بیرونی یکسان را به اشتراک می‌گذارند و همچنین می‌توانند به count کنونی دسترسی پیدا کنند.

function makeCounter() {
  let count = 0;

  function counter() {
    return count++;
  }

  counter.set = value => count = value;

  counter.decrease = () => count--;

  return counter;
}

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

اهمیت: 2

تابع sum را بنویسید که اینگونه کار کند:

sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15

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

باز کردن یک sandbox همراه با تست‌ها.

  1. برای اینکه همه چیز به هر نحوی کار کند، نتیجه sum باید تابع باشد.
  2. آن تابع باید بین فراخوانی‌ها مقدار کنونی را در حافظه ذخیره کند.
  3. با توجه به تمرین، تابع باید زمانی که با == استفاده می‌شود، تبدیل به عدد شود. تابع‌ها شیء هستند پس تبدیل شدن همانطور که در فصل تبدیل شیء به مقدار اصلی گفته شد اتفاق می‌افتد و ما می‌توانیم متد خودمان را برای برگرداندن عدد بسازیم.

حالا می‌رسیم به کد:

function sum(a) {

  let currentSum = a;

  function f(b) {
    currentSum += b;
    return f;
  }

  f.toString = function() {
    return currentSum;
  };

  return f;
}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15

لطفا در نظر داشته باشید که تابع sum فقط یکبار کار می‌کند. تابع f را برمی‌گرداند.

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

هیچ بازگشتی در آخرین خط f وجود ندارد.

بازگشتی اینگونه بنظر می‌رسد:

function f(b) {
  currentSum += b;
  return f(); // <-- فراخوانی بازگشتی
}

و در این مورد ما، بدون صدا زدن تابع، ما فقط آن را برمی‌گردانیم:

function f(b) {
  currentSum += b;
  return f; // <-- خودش را صدا نمی‌زد، خودش را برمی‌گرداند
}

این f در فراخوانی بعدی استفاده می‌شود و دوباره خودش را برمی‌گرداند، هر چند باری که نیاز باشد. سپس زمانی که به عنوان یک عدد یا رشته استفاده می‌شود – toString مقدار currentSum را برمی‌گرداند. ما می‌توانستیم برای تبدیل از Symbol.toPrimitive یا valueOf اینجا استفاده کنیم.

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

نقشه آموزش