ابوالفضل رضوانی نراقی
ابوالفضل رضوانی نراقی
خواندن ۱۴ دقیقه·۵ سال پیش

Hoisting – scopes – closures در زبان javascript


سلام
در مورد تایتل خواستم مطلب بنویسم و یه مقاله پیدا کردم که به نظرم فوق العاده کامل بود و خوب توضیح داده بود بنابر این تصمیم گرفتم اونو براتون ترجمه کنم. لینکشم میگذارم تا اگر از ترجمه خوشتون نیومد اصل مطلب و بخونید.

مقاله زبان اصلی

javascript-visualizer

شاید به نظر عجیب بیاد ولی به نظر من مهم ترین قسمت از اصول اولیه در فهمیدن زبان جوااسکریپت فهمیدن Execution Context باشد. با درک درست از آن میتوانید برای فهمیدن قسمت های پیش رفته جاوااسکریپت در جایگاه درستی قرار بگیرید. با این درک باید به این سوال پاسخ دهیم که اصلا Execution Context چی هست ؟ برای درک درست از آن ابتدا باید ببینیم چگونه یک نرم افزار نوشته می شود.

یک روش در نوشتن یک نرم افزار این است که کد را به قسمت های ریزتری بشکنیم. حالا این قسمت ها ممکنه اسامی مختلفی داشته باشن (توابع، ماژول، پکیج و ... ).

همه آنها برای یک هدف قرار گرفته اند برای شکستن و مدیریت قسمت های پیچیده در نرمافزار.

حالا به جای اینکه مثل کسی که کد میزنه فکر کنیم بیایم مثل موتور جاوااسکریپت که کد رو ترجمه می کنه و به زبان ماشین برگردونه فکر کنیم. آیا میتونیم از همون روش استفاده کنیم و کد رو برای مدیریت پیچیدگی هاش به قسمت های کوچکتری بشکنیم ؟

مشخص میشه که میتونم و به این تکه ها Execution Contextمیگیم.همان طور که توابع ، ماژول ها و پکیج ها به شما اجازه می دهند تا پیچیدگی های کد خودتون رو مدیریت کنید Execution Contexts به موتور جاوااسکریپت اجازه میده تا پیچیدگی ترجمه و اجرای کد شما رو مدیریت کنه. حالا که فهمیدیم Execution Contexts چیه باید به این سوال جواب بدیم که کی ایجاد میشن و شامل چه چیزی هستن ؟

اولین Execution Contexts که ایجاد می شود تا کد شما رو اجرا کند به آن “Global Execution Context” گفته می شود که شامل دو چیز است یک global object و متغییری که به آن this گفته میشود. this به global object رجوع می کند که در محیط مرورگر همان window و در محیط node همان global است.

در تصویر بالا میتوانیم ببینم که بدون هیج کدی Global Execution Context شامل دو چیز میشود window و this. این Global Execution Context در ساده ترین حالتشه.حالا ببینیم چه اتفاقی می افتد وقتی دست به کد می شویم. بگذارید چندتا متغییر اضافه کنیم.

میتونید فرق بین دو تصویر بالا رو تشخیص بدید ؟ فرقش اینه که هر Execution Context دارای دو فاز هست، فاز Creation و فاز Execution که میشه فاز ایجاد و فاز اجرا که هر کدام وظایف خاص خودشونو دارن.

در فاز Global Creation موتور جاوااسکریپت به سکل زیر عمل خواهد کرد.

  • 1- ایجاد Object Global
  • 2- ایجاد Object this
  • 3- قرار دادن فضای مورد نیاز برای متغیر ها و توابع در حافظه
  • 4- اعمال مقدار پیش فرض undefined برای متغییر های تعریف شده در حالی که هر تابع را درون حافظه قرار میدهد.

تا زمانی که وارد فاز Execution نشویم موتور جاوااسکریپت کد شما رو خط به خط اجرا نمی کند در گیف زیر میتوانیم این فازها را مشاهده کنیم.

در طول ایجاد فاز Creation ، window و this ایجاد می شوند. متغییرهایی که تعریف شده اند مقدار undefined میگیرند و هر تابعی که تعریف شده است getUser به طور کامل در حافظه قرار می گیرد. و هنگامی که ما وارد فاز Execution میشویم موتور جاوااسکریپت خط به خط کد ما را بررسی می کند و مقادیری که برای متغییر هایی که اکنون در حافظه هستند تعریف کرده بودیم در آنها قرار می گیرد.

برای اینکه بحث Creation Execution را درک کنید بیایید قبل از مرحله Execution و بعد از مرحله Creation چیزیهایی چاپ کنیم.

console.log('name: ', name) console.log('handle: ', handle) console.log('getUser :', getUser) var name = 'Tyler' var handle = '@tylermcginnis' function getUser () { return {name: name,handle: handle} }

در مرحله فوق چه چیزی توقع دارید تا در console چاپ شود ؟ در زمانی که javascript شروع به اجرای کد ما به شکل خط به خط میکند و console.log() های مارا فراخوانی می کند فاز Creation قبلا اتفاق افتاده است. همان طور که قبلا دیدیم متغییر هایی که تعریف شده بودند باید مقدار undefined میداشتند و توابعی هم که تعریف شده بودند باید کاملا در حافظه قرار می داشتند. پس همانطور که توقع داریم مقدار name و handle برابر با undefined و getUser هم به تابع در حافظه ارجاع داده شده است.

console.log('name: ', name) // name: undefined console.log('handle: ', handle) // handle: undefined console.log('getUser :', getUser) // getUser: ƒ getUser () {} var name = 'Tyler' var handle = '@tylermcginnis' function getUser () { return { name: name, handle: handle, } }

به این مرحله از پردازش که مقدار متغییر های تعریف شده برابر با undefined قرار میگیرید hoisting گفته میشود. چیزی که باعث میشه “hoisting” فهمش گیج کننده باشه اینه که چیزی در حقیقت hoisted نمیشه یا در اطراف حرکت نمی کند. اما حالا که Execution Contexts را می دانید و میدانید که متغییر هایی که تعریف می شوند مقدار undefined در فاز Creation دارند. شما hoisting را متوجه می شوید چون به معنای واقعی همینه که گفتم.

از این نقطه به بعد شما باید با Global Execution Context و فازهای Creation و Execution راحت باشید. خبر خوب اینه که فقط یک Execution Context دیگر باقی مانده است تا یاد بگیرید و اونم عینه Global Execution Context میمونه و بهش Function Execution Context میگن و وقتی ایجاد میشه که تابعی فراخوانی میشه.

تنها زمانی که Execution Context ایجاد و هر زمانی که تابعی فراخوانی شود میشه زمانی هست که موتور جاوااسکریپت شروع به ترجمه کد شما می کند.

حالا سوال اصلی که باید جواب بدیم اینه که چه فرقی بینGlobal Execution Context و یک Function Execution Context است ؟ اگر از قبل به خاطر داشته باشید گفته بودیم که در فاز Global Creation:

  • 1- ایجاد Object Global
  • 2- ایجاد Object this
  • 3- قرار دادن فضای مورد نیاز برای متغیر ها و توابع در حافظه
  • 4- اعمال مقدار پیش فرض undefined برای متغییر های تعریف شده در حالی که هر تابع را درون حافظه قرار میدهد.

کدام مورد هنگامی که در مورد Function Execution Context صحبت می کنیم معنی نمی دهد ؟ دقیقا قدم اول شماره 1.

ما باید فقط یک global object داشته باشیم که آن هم فقط در زمانه فاز Creation در Global Execution Context است نه هر زمانی که تابعی فراخوانی می شود و موتور جاوااسکریپت یک Function Execution Context ایجاد می کند. به جای اینکه یک global object ایجاد کنیم تنها چیزی که یک Function Execution Context نیاز دارد که نگران آن باشد این است که Global Execution Context آرگومان ندارد. با توجه به این میتوانیم لیست قبلی را به این لیست تغییر دهیم:

هنگامی که Function Execution Context ایجاد می شود موتور جاوااسکریپت ..

  • 1- یک global object ایجاد می کند.
  • 1- آبجکت آرگومان ها را ایجاد می کند.
  • 2- آبجکتی به نام this ایجاد میکند.
  • 3- برای متغییر ها و توابع تعریف شده حافظه اختصاص می دهد.
  • 4- هنگامی که توابع تعریف شده را در حافضه قرار می دهد برای متغییر های تعریف شده مقدار پیشفرضundefined را قرار می دهد.

برای دیدن این در عمل به کدی که در قبل داشتیم باز می گردیم ولی این بار به جای اینکه فقط getUser را تعریف کنیم ببنیم چه می شود اگر آن را فراخوانی کنیم.

همانطور که در قبل صحبت کردیم هنگامی که getUser را فراخوانی می کنیم یک Execution Context ایجاد می شود. در طول فاز ایجاد Execution Context برای getUsers ، موتور جاوااسکریپت آبجکتهای this و arguments را ایجاد می کند. به خاطر اینکه getUser متغییری ندارد موتور جاوااسکریپت نیازی ندارد تا فضای حافظه ای تنظیم کند یا متغییر تعریف شده ای “hoist” کند.

شاید متوجه شده باشید که وقتی که اجرای getUser به پایان می رسد از دیده هم خارج می شود. در واقعیت موتور جاوااسکریپت چیزی ایجاد می کند به نام “Execution Stack” (که به “Call Stack” هم معروف است). هر زمان که تابعی فراخوانی می شود یک Execution Context ایجاد می شود و به Execution Stack اضافه می شود. هنگامی که پردازش تابعی در فاز Creation و Execution به اتمام می رسد. از Execution Stack خارج می شود. و از آنجایی که javascript تک نخی است )به این معنی است که فقط یک کار در لحظه میتواند اجرا شود)، که به راحتی قابل مشاهده است.

در اینجا می بینیم که چگونه در فراخوانی ها توابع Execution Context خودشان را درون Execution Stack قرار می دهند. چیزی که هنوز ندیدم این است که متغییر های محلی چه نقشی در آن دارند. بیاید کد را تغییر دهیم تا تابع ما متغییر محلی داشته باشد.


در اینجا نکات ریز و مهمی هست که باید بدانید. هر متغییری که در تابع به عنوان آرگومان پاس بدهید درون تابع به عنوان متغییر محلی خواهد بود. در مثال بالا متغییر handle در Global Execution Context (به خاطر اینکه تعریف شده است) و getURL Execution Context (به خاطر اینکه به عنوان آرگومان) وجود دارد. و نکته بعدی اینکه متغییری که در داخل تابعی تعریف میشود درون Execution Context آن تابع زندگی می کند. پس هنگامی که twitterURL ایجاد میکنیم درون getURL Execution Context زندگی میکند نه در Global Execution Context چرا که آنجا جایی است که تعریف شده است. شاید به نظر واضح باشه ولی از نکات حیاتی مطلب بعدی است.

در گذشته شاید تعریف scope را ‘جایی که به متغییر ها دسترسی داریم’ شنیده باشید. ولی در اصل MDN آن را "context کنونی در حال اجرا " تعریف می کند. ما میتوانیم به “Scope” یا "جایی که به متغییر های دسترسی داریم" همانگونه که به execution contexts فکر می کردیم فکر کنیم.

یه سوال. Bar در کد زیر چه چیزی در console چاپ می کند ؟

function foo () { varbar = 'Declared in foo'; } foo(); console.log(bar)

برسی میکنیم:

هنگامی که foo فراخوانی شد یک Execution Context روی Execution Stack ایجاد کردیم. در فاز ایجاد this و arguments ایجاد شدند و مقدار bar برابر undefined قرار گرفت. بعد فاز اجرا اتفاق افتاد و متن Declared in foo در bar قرار گرفت. بعد از آن فاز اجرا به پایان رسید و foo Execution Context از stack خارج شد. هنگامی که foo از Execution Stack پاک شد ما سعی کردیم که bar را در console چاپ کنیم. این مانند آن است که bar کلا وجود ندارد در نتیجه undefined میگیرم. چیزی که این به ما نشان می هد آن است که متغییر هایی که درون تابع قرار می گیرد به صورت محلی در آن وجود دارند. به این معنیست که (برای اکثر جاهااینگونه است بعدا به حالت استثنا میخوریم) هنگامی که Execution Context یک تابع از Execution Stack خارج شود دیگر به آن دسترسی نداریم..

یه مثال دیگه:

چه چیزی در console چاپ می شود بعد از اینکه اجرای کد تمام شود؟

function first () { var name = 'Jordyn' console.log(name) } function second () { var name = 'Jake' console.log(name) } console.log(name) var name = 'Tyler' first() second() console.log(name)


ما به ترتیب undefined، Jordyn،Jake، Tyler از چپ به راست خواهیم داشت. چیزی که به ما نشان می دهد آن است که شما می توانید به Execution Context به گونه ای فکر کنید که محیط متغییر منحصر به فرد خودش را دارد. اگرچه Execution Contextsهای دیگری هم هستند که شامل متغییر name هستند، اما موتور جاوااسکریپت ابتدا به current Execution Context برای آن متغییر نگاه میکند.

که این سوالی را ایجاد میکند که چه می شود اگر متغییر درون Execution Context کنونی حضور نداشته باشد؟ آیا موتور جاوااسکریپت جستجو در مورد آن متغییر را متوقف می کند ؟ بگذارید مثالی ببینم که به این سوال پاسخ دهد. در کد زیر چه چیزی چاپ میشود ؟

var name = 'Tyler' function logName () { console.log(name) }

این پروسه از موتور جاوااسکریپت دونه به دونه جلو میره و هر Execution Context پدری را بررسی میکند تا ببنید متغییر در Execution Context محلی وجود دارد یا نه که به آن Scope Chain می گوییم. قبلا گفتیم که متغییر هایی که درون تابع تعریف می شوند در اکثر موارد بعد از اینکه Execution Context تابع از درون Execution Stack خارج شود دیگر به آن متغییر دسترسی نداریم اکنون موقع ان است که بگوییم چرا اکثر مواقع اینگونه است نه تمام مواقع. یک سناریو که در آن صدق نمی کند جایی است که شما تابعی درون تابع دیگر داشته باشید. در این مورد تابع درونی همچنان به scope تابع پدر یا بیرونی دست رسی دارد حتی زمانی که Execution Context تابع بیرونی از Execution Stack خارج شود.

توجه کنید که بعد از آنکه makeAdder Execution Context از Execution Stack خارج شد Closure Scope ایجاد شد.

درون Closure Scope متغییر هایی زیست میکنند که درون makeAdder Execution Context زیست میکردند. دیلیلش این است که ما تابعی درون تابع دیگر داریم. در مثال ما تابع درونی به شکل تو در تو درون تابع makeAdder قرار گرفته است بنابراین تابع داخلی یک Closure روی محیط زیست متغییر های makeAdder ایجاد می کند. وقتی این محیط زیست از Execution Stack خارج میشود به خاطر وجود آن Closure تابع داخلی به متغییر X دسترسی دارد (توسط Scope Chain).

همانطور که حدس میزنید این مفهوم که تابع فرزند “closing” روی زیستگاه متغییر های تابع پدر را Closures می خوانند.




javascript
فقط میخوام در آموزش سهمی داشته باشم و اگر بتونم مفید باشم خوشحال میشم.
شاید از این پست‌ها خوشتان بیاید