این this ـِ پرحاشیه (چرا توابع باید bind شوند؟) | مجموعه زیر پوست جاوااسکریپت

بسم الله الرحمن الرحیم

<مقدمه>

موقع تعیین یه تابع به عنوان callback برای یک event با this به مشکل خوردید؟ یا نمیدونید چرا توی ری‌اکت باید this رو bind کرد؟ اینکه react میگه بهتره از arrow function ها برای event ها استفاده نکنید گیج تون کرده؟ یا کلا میخواید بدونید توابع در جاوااسکریپت چطور اجرا میشن؟ این نوشته ها برای شماست :)

اکوسیستم ری‌اکت از مسیر درستش

چند وقت پیش که در رابطه با معماری flux مطالعه می کردم برخوردم به یه repository گیت هاب که مربوط بود به pete hunt (از مدیران فنی فیس بوک که در تیم توسعه ری اکت هم نقش کلیدی داشته و حالا بعد از فروش استارتاپ میلیون دلاری خودش به توییتر، همونجا مشغول کاره) و یه نکته فوق العاده دیدم که توجه منو به خودش جلب کرد.

اکوسیستم ری‌اکت گیج کننده و طاقت فرسا به نظر میرسه چون هیچ وقت از راه درستش آموزش داده نمیشه

این نکته ساده به نظر من فوق العاده است. یکی از علل خستگی جاوااسکریپت (javascript fatigue به معنی سردرگم و خسته شدن توسعه دهنده) فراوانی بسیار زیاد ابزارهای این زبانه.

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

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

منبع محتوای این مطلب کجاست؟

برای این متن از مقاله Understanding JavaScript Function Invocation and "this" که در مستندات خود ری‌اکت بهش رفرنس داده شده، مقاله This is why we need to bind event handlers in Class Components in React از freeCodeCamp و دو سه مقاله انگلیسی معتبر دیگه استفاده شده.

با این مقدمه بحث اصلی امروز رو شروع می کنیم.

</مقدمه>

<متن اصلی>

صدا زدن توابع در پشت صحنه جاوااسکریپت

چیزی که ما بعنوان صدا زدن (function calling or invoking) می شناسیم در واقع یه sugaring برای دستور اصلی هست. sugaring یعنی شیرین کردن! یعنی اینکه یه دستوری در سینتکس زبان به شکلی راحت تر از اون چیزی که واقعا هست بیان میشه. در واقع مثل یه میانبر در نظر بگیریدش. (sugaring تعریف وسیع تری داره برای اینجا همین کافیه) بعدا اون دستور راحت در پشت صحنه تبدیل به دستور اصلی میشه.

همونطور که می بینید روش پایه (primitive) برای صدا زدن توابع اسفاده از متد call هست. بله! روشی که ما تابع رو صدا میزنیم به این روش تبدیل و سپس اجرا میشه و اتفاق مهم همینجا میفته!

تابع call به عنوان ورودی اول this Value رو قبول می کنه و بعد بقیه آرگومان ها رو. یعنی هر چیزی که به عنوان آرگومان اول به این تابع بدیم در بدنه معادل this در نظر گرفته می شه!

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

از طرفی می دونیم اون مدلی که ما توابع رو صدا میزنیم(اسم تابع با پرانتز) اصولا به این مدل (استفاده از call) تبدیل میشه. پس کی آرگومان اول تابع call یا همون مقدار this رو تعیین می کنه؟ خود موتور جاوااسکریپت یه مقداری رو به عنوان this پاس میده به تابع. پس چیزی که javascript به عنوان this پاس مید به تابع کلید ماجراست. حالا کارو یکم پیچیده تر کنیم!

اینجا هم داستان همونه! موتور js تابع رو به وسیله call صدا میزنه و به عنوان this شیئی که صاحب تابع هست رو بهش پاس. این منطقیه! حالا بیاید تابع رو بصورت داینامیک به شئ اضافه کنیم. یعنی بیرون از شئ تابع رو تعریف می کنیم بعد اضافه اش میکنیم به شئ.

تابع آخر که نوشته hello رو همون who در نظر بگیرید و اشتباه منو ببخشید.
تابع آخر که نوشته hello رو همون who در نظر بگیرید و اشتباه منو ببخشید.

میبینیم که صدا زدن تابع who به عنوان عضوی از شئ و به عنوان یک تابع دو مقدار متفاوت رو برای this بر می‌گردونه. اما چرا؟

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

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

جالبه نه؟ در واقع وقتی ما از دستور کوتاه تر برای صدا زدن تابع استفاده می کنیم موتور js بر اساس شرایط در همون زمانِ صدا زدن تصمیم می گیره این this چی باشه!

موتور js تقریبا همیشه undefined رو بعنوان this پاس میده. اما این مقدار فورا به شئ global (مثلا window) تغییر پیدا می کنه. اما اگر در حال استفاده از strict mode باشید این اتفاق نمی افته و this در تابع شما معادل undefined فرض میشه. در اشیاء هم که که مشخصه. وقتی تابعی به عنوان عضوی از یک شئ صدا زده میشه همون شئ بعنوان this درنظر گرفته میشه.

پس this معمولا اینجوری خواهد بود:

  • در همه توابع در scope عمومی: window (یا شئ گلوبال)
  • در توابع عضو یک شئ (یا کلاس): همون شئ
  • در صورت استفاده از strict: کلا undefined .

درسته مسیرش پیچیده بود اما نتیجه نهایی معقوله! نه؟

خب پس چرا توی کامپوننت ها باید this رو bind کنیم؟

دو خط بالاتر گفتیم اگه تابع از یک کلاس باشه this در بدنه تابع میشه معادل همون کلاس پس چرا در ری‌اکت نیاز به bind کردن توابع هست؟

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

تصویر بالا رو خوب ببینید. یه مثال هست صرفا برای درک راحت تر مطلب. فرض کنید وقتی ما یه تابع رو به عنوان callback پاس میدیم یا اصلا به یه متغیر انتساب میدیم صرفا تعریف بدنه تابع میره اونجا میشینه و موقع اجرا هیچ درکی از اینکه این تابع از چه مبدا و کجا اومده وجود نداره. برای همین اگر در بدنه تابع از this استفاده کنیم در واقع یا undefined بر میگرده یا شئ window. نه اون شیئی که ما میخوایم.

این کد رو ببینید. وقتی از نمونه ساخته شده از کلاس مستقیما تابعی که درون خودش this داشت رو فراخوانی کردیم، this به کلاس سازنده اشاره داره. اما وقتی اون رو به تابع دیگه ای پاس دادیم انگار که تابع محیط اصلی خودش رو فراموش کرده باشه، this داره به شی window اشاره می کنه.

اگر این تابع (thisVal) رو به یه متغیر دیگه هم پاس بدیم همین اتفاق می‌افته و اصطلاحا تابع با پاس داده شدن محیط خودش رو از دست میده / فراموش میکنه. (loosing 'this' context)

اینجاست که چند تا راه حل مطرح میشه. رسمی ترین اونها استفاده از bind هست.

استفاده از bind

برای اینکه بتونیم تعیین کنیم در بدنه یک تابع this همیشه و در هرجایی بدون وابستگی به محل فراخوانی به یک شئ مشخص (مثلا شئ والدش) اشاره داشته باشه باید اون تابع رو bind کنیم. احتمالا اینکه چطور این کارو انجام بدیم رو قبلا دیدید پس زیاد راجع بهش صحبت نمی کنیم. بحث ما این بود که چرا این کارو انجام میدیم. بهرحال زحمت جستجو رو براتون کشیدم!! :)

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind

پس نقش Arrow Function ها چیه؟

ذات arrow function ها در مواردی متفاوت با توابع دیگه در جاوااسکریپته. چیزی که اینجا برای ما مهمه اینه که این توابع از lexical this استفاده می کنن. یعنی اینکه کلمه this در این توابع همیشه به نزدیک ترین context اشاره داره. برای همین شما دیگه مشکل bind کردن رو ندارید و سادگی و خوانایی کدتون هم خیلی بهتر میشه.

به این تصویر از مستندات رسمی react نگاه کنید. علت اینکه با استفاده از arrow function مطمئن هستیم که this به جای درستی اشاره می کنه همین lexical this هست.

تابع arrow ما در تابع رندر که متعلق به کلاس loggingButton هست نوشته شده. پس نزدیک ترین context بهش میشه همین کلاس loggingButton . طبق گفته های قبلی this در بدنه این نوع توابع به نزدیک ترین context اشاره میکنه پس this داره به کلاس جاری که دقیقا مد نظر ماست اشاره می کنه. این رفتار خیلی منطقی تر از رفتار طبیعی توابع هست.

پس تموم شد دیگه؟ همین بهترین راهه؟

نه! توابع arrow خیلی خوب و کار راه انداز هستن اما مشکلات خاص خودشون رو هم دارن. در این مورد استفاده از این توابع میتونه دو تا مشکل در خصوص performance کد بوجود بیاره که یکی اش رندر شدن ناخواسته و بیش از کامپوننت هاست.

اما مطلب کِی از arrow function ها در کامپوننت استفاده کنیم؟ کِی نکنیم؟ باشه طلب شما از من. این مطلب جای بیشتر از این صحبت کردن نیست.

به عنوان نکته آخر باید بگم که، راه دیگه ای هم برای خلاص شدن از bind کردن هست. اونم چیزی نیست جز استفاده از class property ها که البته این مورد هنوز آزمایشیه و عضو رسمی زبان ecmascript نیست. هر چند babel میتونه این ویژگی رو برای شما تبدیل به استاندارد های فعلی بکنه. یعنی شما با روش جدید کد میزنید و babel اون رو به binding استاندارد فعلی تبدیل می کنه.

خلاصه که فعلا بهترین روش استاندارد همون bind کردن هست اگر نه از نحوه درست استفاده از arrow ها اطلاع دارید و نه علاقه ای به کانفیگ کردن babel.

من یه تقلب کردم

برای بهتر متوجه شدن شما، من پایه ای ترین حالت صدا زدن تابع رو معادل call در نظر گرفتم. اما این روش یه پله بالاتر از پایه ای ترین حالته. ولی بهرحال برای رسوندن مفهوم انتخاب خوبی بود! (مقاله اصلی که توسط مستندات رسمی React بهش اشاره شده تصمیم گرفته اینطوری مبحث رو باز کنه و منم ازش پیروی کردم!)

ممنونم که تا آخرش خوندید و برای بهتر شدن خودتون یه قدم برداشتید.

و خوشحال میشم اگر نظراتتون رو با من به اشتراک بگذارید و به من برای بهتر شدن کمک کنید ?