تخفیف ویژه برای همه دوره‌ها از شنبه

Arrow Function در جاوااسکریپت با استاندارد ES2015 معرفی شده است و نسبت به توابع معمولی سینتکس ساده‌تری دارد، اما همان‌طور که در این مقاله خواهیم دید، تفاوت‌های مهمی در نحوه عملکرد آن با توابع سنتی وجود دارد.

Arrow Function در جاوااسکریپت چیست؟

در حالی که arrow functionها تقریباً در هر جایی که یک تابع معمولی استفاده می‌شود، قابل استفاده هستند، چند استثنا نیز وجود دارد. این توابع سینتکسی فشرده دارند و مانند توابع معمولی، شامل لیستی از آرگومان‌ها، بدنه تابع و مقدار بازگشتی احتمالی می‌باشند.

در ادامه، جزئیات Arrow Functionها را بررسی خواهیم کرد، اما به‌طور کلی، زمانی که نیاز به یک binding جدید برای this داریم، نباید از آن‌ها استفاده کنیم. Arrow Functionها this مخصوص به خود ندارند و مقدار this را از scope بیرونی خود به ارث می‌برند.

همچنین، از Arrow Functionها نمی‌توانیم به عنوان constructor یا تابع generator استفاده کنیم، زیرا این توابع نمی‌توانند حاوی دستور yield باشند.

بررسی سینتکس بیسیک یک Arrow Function در جاوااسکریپت

یک Arrow Function در جاوااسکریپت شامل لیستی از آرگومان‌ها، یک فلش (=>) و بدنه تابع است. در ادامه یک مثال ساده از یک Arrow Function که یک آرگومان دریافت می‌کند را مشاهده می‌کنیم:

const greet = name => {
  console.log(`Hello, ${name}!`);
};

ما می‌توانیم در صورت تمایل آرگومان را داخل پرانتز قرار دهیم:

const greet = (name) => {
  console.log(`Hello, ${name}!`);
}

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

const sum = (a, b) => {
  return a + b;
}

یک anonymous arrow function نامی ندارد و معمولاً به عنوان تابع callback استفاده می‌شود:

button.addEventListener('click', event => {
  console.log('You clicked the button!');
});

اگر بدنه Arrow Function شامل یک statement منفرد باشد، نیازی به {} نداریم:

const greet = name => console.log(`Hello, ${name}!`);

بازگشت ضمنی در Arrow Function جاوااسکریپت

یکی از تفاوت‌های مهم بین Arrow Function و یک تابع معمولی در جاوااسکریپت، مفهوم بازگشت ضمنی (Implicit Return) است. در این روش، مقدار تابع بدون نیاز به نوشتن return، به طور خودکار بازگردانده می‌شود.

اگر {} را در یک Arrow Function حذف کنیم، مقدار عبارت داخل بدنه تابع به طور خودکار بازگردانده می‌شود. به عنوان مثال، تابع sum را که قبلاً نوشتیم، دوباره با این روش بازنویسی می‌کنیم:

const sum = (a, b) => a + b;

بازگشت ضمنی بسیار مفید است، به‌ویژه هنگام تعریف توابع callback. برای مثال:

const values = [1, 2, 3];
const doubled values = values.map(value => value * 2); // [2, 4, 6]

بازگرداندن یک آبجکت به صورت ضمنی

در بازگشت ضمنی، می‌توانیم هر نوع مقداری را return کنیم، اما برای آبجکت‌ها یک نکته مهم وجود دارد. از آنجایی که آبجکت‌ها در جاوااسکریپت با {} مشخص می‌شوند، اگر به طور مستقیم {} را در تابع بنویسیم، جاوااسکریپت آن را به عنوان بدنه تابع در نظر می‌گیرد و مقدار undefined برمی‌گرداند:

کد اشتباه:

const createUser = (name, email) => { name, email };

در این حالت، هیچ بازگشت ضمنی وجود نخواهد داشت و تابع در واقع undefined برمی‌گردد؛ زیرا، هیچ عبارت return وجود ندارد. برای return کردن آبجکت‌ها در Arrow Functionها، باید آن‌ها را داخل پرانتز قرار دهیم:

کد درست:

const createUser = (name, email) => ({ name, email });

اکنون جاوااسکریپت می‌داند که این یک بازگشت ضمنی از یک آبجکت است که حاوی ویژگی‌های name و email می‌باشد.

بازگشت صریح (Explicit Return) در Arrow Functionها

درست مانند توابع معمولی، Arrow Functionها نیز می‌توانند به صورت صریح، یعنی با استفاده از return مقداری را بازگردانند:

const createUser = (name, email) => {
  return { name, email };
};

تفاوت‌های Arrow Functionها و توابع معمولی در جاوااسکریپت

علاوه بر سینتکس ساده‌تر، Arrow Functionها تفاوت‌های مهمی با توابع معمولی دارند که در ادامه به بررسی آن‌ها می‌پردازیم.

عدم ایجاد this جدید

مهم‌ترین تفاوت Arrow Functionها این است که برخلاف توابع معمولی، یک this جدید ایجاد نمی‌کنند. بلکه مقدار this را از scope بیرونی به ارث می‌برند. مثلا:

const counter = {
  value: 0,
  increment: () => {
    this.value += 1;
  }
};

در این مثال، this در تابع increment به counter اشاره نمی‌کند؛ بلکه this را از scope بیرونی (که در این مثال آبجکت window سراسری است) به ارث می‌برد. به همین دلیل مقدار counter.value تغییر نمی‌کند و مقدار undefined خواهد بود.

گاهی این ویژگی مفید است، مثلاً هنگام استفاده از توابع callback. در گذشته، برای حفظ مقدار this درون توابع، از bind استفاده می‌کردیم:

روش قدیمی:

var self = this;
setTimeout(function() {
  console.log(self.name);
}, ۱۰۰۰);

روش جدید با استفاده از Arrow Functionها:

setTimeout(() => console.log(this.name));

در این مثال، this از scope بیرونی به ارث برده می‌شود و نیازی به bind نیست.

عدم وجود آبجکت arguments

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

به عنوان مثال، تابع sum که تعداد متغیری از آرگومان‌ها را دریافت می‌کند:

function sum() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }

  return total;
}

می‌توانیم این تابع را با هر تعداد آرگومان فراخوانی کنیم:

sum(1, 2, 3) // 6

چرا arguments در Arrow Functionها وجود ندارد؟

اگر این تابع را به یک Arrow Function تبدیل کنیم، دیگر آبجکت arguments وجود نخواهد داشت. بنابراین، باید از سینتکس rest parameter استفاده کنیم:

const sum = (...args) => {
  let total = 0;
  for (let i = 0; i < args.length; i++) {
    total += args[i];
  }

  return args;
}

فراخوانی این نسخه از تابع sum مانند قبل خواهد بود:

sum(1, 2, 3) // 6

نکته‌ای که باید به آن توجه داشته باشیم این است که این سینتکس مخصوص Arrow Functionها نیست و در توابع معمولی هم می‌توان از سینتکس rest parameter استفاده کرد. در جاوااسکریپت مدرن، دیگر از arguments خیلی کم استفاده می‌شود، بنابراین این تفاوت اهمیت زیادی ندارد.

عدم وجود prototype

توابع معمولی جاوااسکریپت دارای ویژگی prototype هستند. قبل از معرفی سینتکس class، از این ویژگی به همراه کلمه کلیدی new برای ایجاد آبجکت‌هایی که قابلیت نمونه‌سازی دارند، استفاده می‌شد. به عنوان مثال:

function Greeter() { }
Greeter.prototype.sayHello = function(name) {
  console.log(`Hello, ${name}!`);
};

new Greeter().sayHello('Joe'); // Hello, Joe!

اگر همین کار را با Arrow Functionها انجام دهیم، با خطا مواجه خواهیم شد. زیرا، Arrow Functionها prototype ندارند:

const Greeter = () => {};
Greeter.prototype.sayHello = name => console.log(`Hello, ${name}!`);
// TypeError: Cannot set properties of undefined (setting 'sayHello')

در نتیجه، از آنجایی که Arrow Functionها دارای prototype نیستند، نمی‌توانیم از آن‌ها برای ایجاد کلاس‌ها یا آبجکت‌هایی که قابلیت نمونه‌سازی دارند، استفاده کنیم. برای این کار، بهتر است از توابع معمولی یا کلاس‌ها استفاده نماییم.

چه زمانی از باید Arrow Functionها استفاده کنیم و چه زمانی از توابع معمولی؟

Arrow Functionها در بسیاری از سناریوها کاربرد دارند، اما در برخی موارد باید از توابع معمولی استفاده کنیم. این موارد عبارتند از:

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

  1. در constructor یک کلاس
  2. در متدهای یک آبجکت، زمانی که نیاز داریم از this برای دسترسی به خود آبجکت استفاده کنیم
  3. در توابعی که باید this را صراحتاً با Function.prototype.bind متصل کنیم
  4. در توابع generator که شامل yield هستند

اما Arrow Functionها در جاهایی مثل توابع callback به شدت کارآمدند، چون سینتکس کوتاه‌تری دارند و کد را خواناتر می‌کنند. به طور خاص، برای متدهای آرایه‌ای مثل forEach، map و filter بسیار مفیدند. می‌توانیم از آن‌ها در متدهای آبجکت هم استفاده کنیم، اما فقط اگر متد نیاز نداشته باشد this را برای دسترسی به آبجکت استفاده کند.

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

چگونه یک متد را با استفاده از Arrow Function تعریف کنیم؟

در برخی موارد، می‌توانیم متدهای یک کلاس را با Arrow Functionها تعریف کنیم. مثلا:

class Person {
  constructor(name) {
    this.name = name;
  }

  greet = () => console.log(`Hello, ${this.name}!`);
}

برخلاف متدهای معمولی در یک آبجکت literal، اینجا greet مقدار this را از کلاس Person به ارث می‌برد. این یعنی فارغ از اینکه متد چگونه فراخوانی شود، مقدار this همیشه به نمونه کلاس اشاره می‌کند. این مثال را در نظر می‌گیریم که از یک متد استاندارد با setTimeout استفاده می‌کند:

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
      console.log(`Hello, ${this.name}!`);
  }

  delayedGreet() {
      setTimeout(this.greet, 1000);
  }
}

new Person('Joe').delayedGreet(); // Hello, undefined!

وقتی متد greet از طریق setTimeout اجرا می‌شود، مقدار this به آبجکت window اشاره می‌کند.
از آنجایی که در window ویژگی به نام name وجود ندارد، نتیجه‌ Hello, undefined! خواهد شد.

اگر greet را به عنوان یک Arrow Function تعریف کنیم، this مقدار کلاس Person را حفظ می‌کند، حتی اگر متد در setTimeout اجرا شود:

class Person {
    constructor(name) {
      this.name = name;
    }

    greet = () => console.log(`Hello, ${this.name}!`);

    delayedGreet() {
        setTimeout(this.greet, 1000);
    }
}

new Person('Joe').delayedGreet(); // Hello, Joe!

با Arrow Function، مقدار this از Person حفظ می‌شود و متد greet همچنان به نام فرد دسترسی خواهد داشت.

اما نمی‌توانیم constructor را با Arrow Function بنویسیم؛ اگر بخواهیم این کار را انجام دهیم، با خطا مواجه خواهیم شد:

class Person {
  constructor = name => {
    this.name = name;
  }
}

// SyntaxError: Classes may not have a field named 'constructor'

جمع‌بندی

از زمان معرفی استاندارد ES2015، برنامه‌نویسان جاوااسکریپت به Arrow Functionها دسترسی دارند. مزیت اصلی آن‌ها سینتکس مختصر و خوانا است؛ یعنی دیگر نیازی به استفاده از function نیست و در مواردی با بازگرداندن مقدار ضمنی، نیازی به نوشتن return هم نخواهد بود.

نبود مقدار this اختصاصی می‌تواند در برخی موارد چالش‌برانگیز باشد، اما در سناریوهایی که نیاز به حفظ مقدار this از scope بالاتر داریم، این ویژگی می‌تواند بسیار مفید باشد.

مثال: استفاده از Arrow Function برای ساده‌سازی کد

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

const numbers = [1, 2, 3, 4]
  .map(function(n) {
    return n * 3;
  })
  .filter(function(n) {
    return n % 2 === 0;
  });

با استفاده از Arrow Function کد بسیار تمیزتر و خواناتر می‌شود:

const numbers = [1, 2, 3, 4]
  .map(n => n * 3)
  .filter(n => n % 2 === 0);

Arrow Functionها فاقد آبجکت arguments هستند، اما می‌توانیم از سینتکس rest parameter برای دریافت تعداد متغیر از آرگومان‌ها استفاده کنیم.

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