37 - جاوااسکریپت: Objects, Functions, this

گفتیم که تابع یک شیء است. حالا میخواهیم بدانیم که وقتی قسمت CODE تابع invoce میشود چه اتفاقی می افتد. وقتی تابعی فراخوانی میشود یک execution context جدید ایجاد میشود. شیء ای که تابع است متدها و ویژگی هایی دارد. ویژگی NAME و CODE دارد. وقتی که تابع invoce میشود، execution context ایجاد میشود و در استک execution context قرار میگیرد. پس execution context را با توجه به قسمت CODE نگاه میکنیم. وقتی کد داخل CODE را اجرا میکنیم چه اتفاقی می افتد؟ میدانیم که execution context ایجاد میشود. هر execution context یک variable environment دارد. یعنی جایی که متغیرهای داخل تابع ایجاد میشوند. همچنین reference ای به outer environment دارد. با توجه به جای فیزیکی و lexical کد تعیین میشود و scope chain را ایجاد میکند. یعنی اگر دنبال متغیری میگردیم که داخل variable environment تابع نیست، تا global environment بیرون میرود و دنبالش میگردد. همچنین موتور js هر موقع که execution context ایجاد میشود (هر موقع که تابع اجرا میشود) به ما (بدون نیاز به ایجاد کردن) متغیر this را میدهد. متغیر this بسته به اینکه تابع چگونه فراخوانی میشود به چیزی اشاره میکند.چند سناریو وجود دارد که this را تغییر میدهد، بسته به اینکه تابع چگونه فراخوانی شود و کجا قرار دارشته باشد.اگر در سطح global execution context داشته باشیم:console.log(this);به ما خروجی شیء Window را میدهد. چون در این سطح این this به global object اشاره میکند و داخل بروزر global object همان شیء Window است. حال اگر یک تابع داشته باشیم:function a() { console.log(this); }a();با فراخوانی تابع a ویژگی CODE (حاوی همه خطوط کد داخل تابع) اجرا میشود و ابتدا execution context ایجاد میشود که یکی از قسمت های آن ایجاد this است. این this داخل execution context مربوط به فراخوانی تابع a به چی اشاره میکند؟ در کنسول میبینیم که خروجی کد بالا باز هم شیء Window است. بنابراین وقتی یک تابع ایجاد میکنیم، کلمه کلیدی this داخل آن به global object اشاره میکند. همین اتفاق وقتی از function expression استفاده میکنیم هم رخ میده:var b = function() { console.log(this); }در این کد متغیر b این function expression را میگیرد تا آن را ایجاد کند. اگر این تابع را invoke کنیم باز هم خروجی Window داده میشود. بنابراین هر موقع که در این سطح از کد تابعی ایجاد کنیم (با function expression یا با function statement)، داخل این تابع ها this به global object اشاره میکند. پس حتی وقتی سه تا execution context داریم (یکی global و دو تا برای تابع های a و b)، با اینکه هر کدام از آنها یک this خودشان را دارند، اما در هر سه آنها this به یک آدرس یکسان اشاره میکند. پس همه به global object اشاره میکنند. بنابراین مثلا میتوانیم در کد تابع a اضافه کنیم:function a() {console.log(this);this.newVariable = 'hello';}متغیر newVariable را به global object مان attach کرده ایم. پس میتوانیم در سطح global از آن متغیر استفاده کنیم:console.log(newVariable);به هر متغیری که به global object مان attach شده باشد میتوانیم فقط با صدا زدن اسمش دسترسی داشته باشیم و نیازی به استفاده از عملگر . نیست. خروجی به ما hello را میدهد. پس وقتی که فقط یک تابع را invoke میکنیم، this به global variable اشاره میکند.اگر داخل شیء مقدار primitive باشد property و اگر تابع باشد method نام دارد. مثلا:var c = { name: 'The c object', log: function() { console.log(this); }}c.log();همان طور که گفته هر موقع که تابعی invoke شود یک execution context جدید ایجاد میشود و موتور js تصمیم میگیرد که این کلمه کلیدی باید چه چیزی اشاره کند. در مثالهای قبلی پاسخ شیء Window بود. اما در این حالت یک متد در یک تابع داریم. خروجی Object{name:'The c object', log:function} است. در حالتی که تابع یک متد اتچ شده به یک شیء باشد، کلمه کلیدی this همان شیئی خواهد بود که آن متد داخل آن است. یعنی همان شیئی که شامل آن تابع است. پس میتوانیم داشته باشیم:var c = { name: 'The c object', log: function() { this.name = 'updated c object'; }}c.log();و خروجی Object{name:'updated c object', log: function} است. property شیء پدر را تغییر دادیم. شیئی که شامل تابع بود. پس اگر متدی در یک شیء باشیم، میتوانیم با کلمه کلیدی this شیء نگهدارنده را mutate کنیم. یعنی میتوانیم به property ها و متدهای دیگر همان شیء دسترسی داشته باشیم.یک نکته دیگر هم در این مورد this هست که اگر در موردش چیزی ندانیم فکر میکنیم باگ است. اما js یک زبان برنامه نویسی است و موتورهای زبان ها توسط افراد نوشته شده اند. افراد در مورد چگونگی کار کردن آنها تصمیم گرفته اند. در این مورد افراد زیادی احساس میکنند که تصمیم گیری درست انجام نشده است.فرض کنید یک تابع داخل یک متد ایجاد کرده ایم:var c = { name: 'The c object', log: function() { this.name = 'updated c object'; var setName = function(newName) { this.name = newName; } setName('updated again'); console.log(this); }}c.log();در کد بالا سعی کرده ایم که شیء مان را mutate کنیم. در این کد انتظار داریم که this نوشته شده داخل setName به شیء c اشاره کند. چون این یک تابع داخل یک تابع دیگر داخل یک شیء است. همان طور که this داخل log به شیء اشاره میکند، انتظار داریم که در یک سطح عمیقتر هم در تابع this به شیء اشاره کند. پس انتظار داریم که بعد از فراخوانی setName و گرفتن console.log اسم updated again را برای c ببینیم. اما در خروجی داریم:Object { name: 'updated c object', log: function }به نظر میرسد که setName('updated again'); هیچ کاری انجام نداده. اما انجام داده. اگر در قسمت کنسول window را بنویسیم، شیء global پنجره را به ما نشان میدهد و به ویژگی های آن name: 'updated again' اضافه شده. یعنی وقتی execution context این تابع internal ایجاد شد، کلمه کلیدی this در آن به global object اشاره میکند. حتی وقتی که داخل شیئی است که ایجاد کرده ایم. خیلی افراد فکر میکنند که این اشتباه است ولی این روشی است که js با آن کار میکند. پس در چنین سناریویی چه کار میتوانیم انجام دهیم تا مطمئن باشیم از شیء درست استفاده میکنیم و کلمه this اونطور که میخوایم اشاره میکنه؟ یک الگوی خیلی رایج در این حالت استفاده میشود. چون ما میدانیم که شیء با by reference کار میکند پس میتوانیم این الگو را بفهمیم. همان خط اول متدمان this را داخل متغیری به اسم that یا self میریزیم. با این کار یک متغیر جدید داریم به نام self و چون برابر با یک شیء قرار داده شده پس by reference است. یعنی self به همان مکانی از حافظه اشاره میکنید که کلمه کلیدی this اشاره میکند. در این خط از کد this به شیء نگهدارنده یعنی c اشاره میکند. سپس در ادامه کد به جای this از self استفاده میکنیم. حتی داخل تابعی که در آن تعریف شده:var c = { name: 'The c object', log: function() { var self = this; self.name = 'updated c object'; var setName = function(newName) { self.name = newName; } setName('updated again'); console.log(self); }}c.log();بنابراین دیگه نیازی نیست فکر کنیم آیا داریم به شیء درستی اشاره میکنیم یا نه. در setName هم که از self استفاده شده نگرانی در مورد متغیر self نداریم. self داخل این تابع تعریف نشده، پس موتور js در scope chain پایین میره. تابع setName به طور فیزیکی کجای کد نوشته شده؟ outer lexical reference آن به log برمیگردد و مقدار self را پیدا میکند. پس داخل تابع setName هم میتوانیم شیءمان را mutate کنیم. پس یک رفرنس مناسب به شیءمان تهیه کردیم: var self = this و بعد از آن ازش استفاده کردیم. پس در این حالت خروجی:Object { name: 'updated again', log: function }هیچ زبان برنامه نویسی ای بی نقص نیست و js هم استثنا نیست. اما الگوهایی وجود دارند که میتوانیم از آنها برای مدیریت مشکل زبان استفاده کنیم. این الگو که اینجا گفته شد خیلی زیاد استفاده میشود. کلمه کلیدی let که جایگزینی برای var است برخی از این مشکلات را برطرف میکند. بنابراین با در دسترس قرار گرفتن let در بروزرهای مدرن و بسته به نوع پروژه (مثلا پروژه ای که در آن نگرانی در مورد بروزرهای قدیمی نداریم) میتوانیم از آن استفاده کنیم. اما در حال حاضر همین الگو روش مناسبی است.پس دیدیم وقتی که فقط یک تابع را invoke میکنیم، کلمه کلیدی this همان global variable در global object است. وقتی که تابع یک متد از یک شیء باشد، کلمه کلیدی this به همان شیء اشاره میکند. اما هر تابع internal ای مشکلی دارد، پس میتوانیم از مفهوم مساوی قرار دادن یک متغیر با this استفاده کنیم تا مطمئن باشیم به خطایی برنمیخوریم یا به اشتباه با global object کار نکرده ایم.