Arrow Function و this: قانونی متفاوت از همه‌ی قوانین قبلی

مقدمه

در چهار درس قبل، چهار قانون Binding را دیدیم که همگی یک ویژگی مشترک داشتند: مقدار this بر اساس نحوه‌ی فراخوانی تابع تعیین می‌شد. در فصل توابع پیشرفته، هنگام معرفی Arrow Function، به‌طور مختصر اشاره کردیم که این نوع تابع، this مستقل خودش را ندارد. اکنون وقت آن رسیده تا این رفتار را دقیق و کامل بررسی کنیم؛ رفتاری که کاملاً از چهار قانون قبلی مستقل است.

قانون Arrow Function: this از Lexical Scope گرفته می‌شود

Arrow Function اصلاً this مخصوص به خودش نمی‌سازد. به‌جای آن، this داخل یک Arrow Function، دقیقاً همان مقداری است که در Lexical Scope بیرونی (یعنی جایی که Arrow Function تعریف شده) وجود دارد. به بیان دیگر، Arrow Function از نظر this، از همان قانون Lexical Scope که در فصل Execution Model برای متغیرها دیدیم پیروی می‌کند؛ نه از قوانین Dynamic Binding که در چهار درس قبل بررسی کردیم.

Arrow Function، this را از بیرون به ارث می‌برد
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل کد

با اینکه greetArrow از طریق user.greetArrow() فراخوانی شده -یعنی دقیقاً همان شکلی که در درس Implicit Binding، انتظار داشتیم this را برابر user کند- اما چون greetArrow یک Arrow Function است، این قانون اصلاً اعمال نمی‌شود. Arrow Function به Lexical Scope بیرونی خودش نگاه می‌کند تا this را پیدا کند؛ و از آن‌جا که این Object Literal در سطح Global تعریف شده، this داخل greetArrow همان this سطح Global خواهد بود (نه user).

این نکته‌ی بسیار مهمی است: نحوه‌ی فراخوانی یک Arrow Function، هیچ تأثیری روی مقدار this داخل آن ندارد. حتی اگر با call، apply یا bind هم تلاش کنیم this آن را صریحاً تغییر دهیم، Arrow Function این تلاش را نادیده می‌گیرد؛ چون از همان ابتدا اصلاً this مستقلی برای Bind شدن ندارد.

کاربرد اصلی و مفید: حفظ this در توابع تودرتو

همین ویژگی که در نگاه اول ممکن است محدودیت به‌نظر برسد، در بسیاری از موقعیت‌ها دقیقاً همان چیزی است که نیاز داریم. یکی از رایج‌ترین مشکلات پیش از رواج Arrow Function، از دست رفتن this در یک تابع معمولی تودرتو بود:

مشکل کلاسیک this در تابع معمولی تودرتو
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل رفتار جاوااسکریپت

اگر این کد را اجرا کنید، به‌جای افزایش منظم عدد، با خطا مواجه می‌شوید (یا در غیر-Strict Mode، مقدار NaN مشاهده می‌کنید). دلیل این است که تابع معمولی داخل setInterval، یک Function Invocation جداگانه است (دقیقاً طبق قانون Default Binding که در درس‌های قبل دیدیم)؛ به همین دلیل this داخل آن، دیگر به timer اشاره ندارد، بلکه به شیء Global اشاره می‌کند که seconds ندارد.

حالا همین کد را با Arrow Function ببینید:

حل مشکل با استفاده از Arrow Function
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل کد

در این نسخه، چون تابع داخل setInterval یک Arrow Function است، این تابع اصلاً this مستقلی نمی‌سازد؛ بلکه this را از نزدیک‌ترین Scope تابعی بیرونی خودش، یعنی تابع start، به ارث می‌برد. و چون start خودش یک تابع معمولی است که با timer.start() فراخوانی شده (طبق Implicit Binding)، this داخل start برابر timer است. در نتیجه، Arrow Function داخل آن هم همان this (یعنی timer) را به ارث می‌برد و this.seconds++ به‌درستی روی timer.seconds عمل می‌کند.

نکته‌ی مهم: Arrow Function خودش هیچ Binding ای ایجاد نمی‌کند

دقت کنید که راه‌حل بالا، به این دلیل کار کرد که Arrow Function، this را از تابع معمولی بیرونی (یعنی start) گرفت که خودش از قبل، از طریق Implicit Binding، this درستی داشت. اگر تابع بیرونی خودش هم this نادرستی داشته باشد، Arrow Function داخلی هم همان this نادرست را به ارث می‌برد؛ چون Arrow Function هیچ مکانیزم مستقلی برای «تصحیح» this ندارد، فقط آن را از بیرون کپی می‌کند.

چرا نباید Arrow Function را به‌عنوان متد شیء تعریف کرد؟

همان‌طور که در مثال اول این درس دیدیم، تعریف یک متد شیء با Arrow Function معمولاً اشتباه است؛ چون دقیقاً همان چیزی را که Implicit Binding قرار بود فراهم کند (یعنی this برابر با شیء صاحب متد)، از بین می‌برد:

let user = {
  name: "علی",
  // اشتباه: Arrow Function به‌عنوان متد
  greetWrong: () => {
    console.log(this.name); // this به user اشاره نمی‌کند
  },
  // درست: Function Expression یا Method Shorthand
  greetCorrect: function () {
    console.log(this.name); // this به‌درستی به user اشاره می‌کند
  }
};

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

جدول جمع‌بندی: Arrow Function در مقابل چهار قانون سنتی

ویژگیتابع معمولیArrow Function
دارای this مستقلبلهخیر
تأثیرپذیری از نحوه‌ی فراخوانیبله (چهار قانون Binding)خیر
تأثیرپذیری از call/apply/bindبلهخیر (نادیده گرفته می‌شود)
منبع thisمحل و نحوه‌ی فراخوانیLexical Scope محل تعریف
مناسب برای متد شیءبلهمعمولاً خیر
مناسب برای Callback تودرتو (حفظ this بیرونی)نیاز به راه‌حل اضافی (مثل bind)بله، به‌طور طبیعی

ارتباط با سایر مفاهیم

این درس، فصل this و Binding را با روشن کردن یک استثنای کامل بر تمام قوانین قبلی به پایان می‌رساند. درک این تفاوت، در فصل‌های بعدی، به‌ویژه هنگام کار با Event Listener ها در Browser (جایی که انتخاب بین Arrow Function و تابع معمولی برای Callback اهمیت مستقیم دارد) و همچنین در فصل Object Model هنگام تعریف متدهای Class، به‌طور مکرر کاربرد خواهد داشت.

جمع‌بندی

Arrow Function برخلاف چهار قانون Binding که در این فصل بررسی کردیم، اصلاً this مستقل خودش را نمی‌سازد؛ بلکه آن را از Lexical Scope محل تعریف خودش به ارث می‌برد و هیچ‌گاه تحت تأثیر نحوه‌ی فراخوانی یا حتی call/apply/bind قرار نمی‌گیرد. این ویژگی، Arrow Function را برای Callback های تودرتو که باید this تابع بیرونی را حفظ کنند بسیار مناسب می‌کند، اما آن را برای تعریف مستقیم متد یک شیء، نامناسب می‌سازد.