بررسی مفهوم Execution Context در جاوااسکریپت - پشت‌ پرده جاوااسکریپت (بخش اول)

در این مقاله که به خاطر زیاد بودن مطالب مجبور شدم تو چند پست جدا بنویسم، می‌خوام کمی متفاوت‌تر از آموزش‌های موجود، زبان جاوااسکریپت رو باهم بررسی کنیم. این مطالب و مفاهیم در برخورد اول برای دولوپرهای تازه‌کاری مثل من، شاید گیج‌کننده باشن و نتایجی هم که خواهیم‌گرفت ساده به نظر برسن و احتمالا قبلا هم می‌دونستین ولی هدف از این پست اینه که مسائل رو کنار نتایج قرار بدیم تا دیدمون نسبت به جاوااسکریپت وسیع‌تر بشه و بهتر از این زبان استفاده کنیم.

همه کدهای جاوا اسکریپت نیاز به میزبانی و اجرا در نوعی محیط دارند. در بیشتر موارد، آن محیط یک مرورگر وب خواهد بود.

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

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

(برنامه تجزیه و تحلیل) Parser : یک Parser یا Syntax Parser برنامه ای است که کد شما را خط به خط می خواند. درک می کند که چگونه کد با نحو تعریف شده توسط زبان برنامه نویسی مطابقت دارد و انتظار می رود آن (کد) چه کاری انجام دهد.

(موتور جاوا اسکریپت) JavaScript Engine : موتور جاوا اسکریپت به سادگی یک برنامه کامپیوتری است که کد منبع جاوا اسکریپت را دریافت می کند و آن را به دستورالعمل های باینری (کد ماشین) که CPU می تواند درک کند، کامپایل می کند. موتورهای جاوا اسکریپت معمولاً توسط فروشندگان (vendors) مرورگر وب توسعه می یابند و هر مرورگر اصلی یک موتور جاوا اسکریپت دارد. به عنوان مثال می توان به موتور V8 برای گوگل کروم، اسپایدرمونکی برای فایرفاکس و چاکرا برای اینترنت اکسپلورر اشاره کرد.

(تابع های با اسم) Function Declarations : اینها توابعی هستند که یک نام به آنها اختصاص داده شده است.

function doSomething() { //here &quotdoSomething&quot is the function's name 
    statements;
}

(تابع های بدون اسم) Function Expressions : اینها توابع ناشناس هستند، یعنی توابعی بدون نام تابع مانند js function () { statements } . آنها معمولاً در عبارات استفاده می شوند، مانند تخصیص یک تابع به یک متغیر. let someValue = function () { statements }.

خب بریم سر اصل مطلب!


Execution Context:

در جاوااسکریپت execution context یک مفهوم انتزاعی و فرضی است که اطلاعات (this keyword, objects variables, functions) قسمتی از کد که در حال تفسیر و اجرا توسط موتور جاوااسکریپت (javascript engine) است در آن ذخیره شده. به عبارت بهتر هر زمانی که کد در جاوااسکریپت اجرا می‌شه در واقع داخل یک execution context این اتفاق می‌افته.

برای درک بهتر می‌تونیم execution context رو یک جعبه در نظر بگیریم که یه سری کد داخلش نگهداری می‌شه و وقتی نوبت اجرا و تفسیرش می‌رسه اون اطلاعات رو در اختیار مفسر یا همون موتور جاوااسکریپت (javascript engine) قرار می‌ده.

می‌شه execution context رو به عنوان یک شئ (object) هم در نظر گرفت که ویژگی‌هایی (properties) هم داره. فعلا این رو تو ذهنتون نگه‌دارین، بعدا راجع به این موضوع بیشتر صحبت می‌کنیم.

در جاوااسکریپت 3 نوع execution context داریم:

  • Global Execution Context
  • Functional Execution Context
  • Eval Function Execution Context


Global Execution Context (GEC):

هر زمان که موتور جاوا اسکریپت یک فایل اسکریپت دریافت می کند، ابتدا یک default Execution Context به نام Global Execution Context (GEC)ایجاد می کند.

این GEC یک base/default Execution Context است که در آن تمام کدهای جاوا اسکریپتی که داخل یک تابع نیستند اجرا می‌شوند.

در مرورگر execution context پیش‌فرض، global execution context هست. کدی که داخل هیچ فانکشنی نوشته نشده، در global execution context ذخیره می‌شه. اگر execution context رو به عنوان object درنظر بگیریم، global execution context هم به عنوان global object شناخته می‌شه.

* در مرورگر، window object همون global object هست.

* در هر صفحه وب فقط یک global object وجود داره.

اگه کد زیر رو تو کنسول مرورگر بنویسین، window object رو به عنوان نتیجه می‌بینین:

console.log(this);     //return window object


Functional Execution Context (FEC):

هر زمان که یک تابع فراخوانی می شود، موتور جاوا اسکریپت نوع متفاوتی از Execution Context را ایجاد می کند که به عنوان یک Function Execution Context (FEC) در GEC شناخته می شود تا کد را در آن تابع ارزیابی و اجرا کند.

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

هر فانکشن یه execution context مختص به خودش رو داره ولی تا زمانی که فانکشن فراخوانی نشده، execution context اون فانکشن ساخته نمی‌شه. تو این مقاله ما بیشتر راجع به این نوع execution context صحبت می‌کنیم.

پس هر وقت یک فانکشن فراخوانی شد یه execution context مختص به اون فانکشن هم ساخته می‌شه.

با کمی دقت به تصویر بالا به این نتیجه می‌رسیم که در هر برنامه به تعداد فراخوانی‌های فانکشن‌ها، functional execution context داریم. یعنی اگر تعداد فراخوانی‌ها 10 تا باشه به همون تعداد هم functional execution context داریم.

** چون فانکشن ()eval خیلی کم کاربرد داره و همچنین فانکشن دردسرسازی هم هست، درباه‌ی Eval Function Execution Context بحث نمی‌کنیم. اگه می‌خاین دربارش بیشتر بدونین، اینجا رو مطالعه کنین.

خب تا این جای کار با execution context و انواع اون آشنا شدیم و کاربرد هر کدوم رو فهمیدیم. حالا بریم درباره‌ی یک مفهوم دیگه به نام execution stack صحبت کنیم.


Execution Stack:

یک ساختار داده‌ای هست که execution context هایی که به وسیله‌ی فراخوانی فانکشن‌ها ساخته شدن، در این ساختار و بستر ذخیره می‌شن. execution stack از مکانیزم LIFO) Last In, First Out) استفاده می‌کنه.

برای درک بهتر، execution stack رو به عنوان یک دودکش! در نظر بگیریم که execution context ها به ترتیب از بالای دودکش میان و می‌رن اون پایین قرار می‌گیرن و ذخیره می‌شن و هنگام برگشت داده شدن (return)، آخرین و بالاترین execution context، اولین execution context خواهد بود که برگشت داده می‌شه که همون مکانیزم LIFO هست.

* باید به این نکته توجه داشت که همیشه global execution context، اولین execution context ذخیره شده در execution stack هست.

خب حالا بیاین کد زیر رو با توجه به گفته‌های بالا بررسی کنیم:

var a = 'message';

function first () {
 var b = 'first function';
 second(); var x = b + a;
}

function second () {
 var c = 'second function';
 third(); var y = c + a;
}

function third () {
 var d = 'third function';
 var z = d + a;
}

first();

1- مرحله اول:

در اولین مرحله کدهایی که داخل هیچ فانکشنی نیستن، تو global execution context ذخیره می‌شن. یعنی از خط 1 کدمون تا قبل از فراخوانی فانکشن ()first.

باید به این نکته توجه داشته باشیم همون طور که قبلا هم بررسی کردیم، نوشتن یک فانکشن باعث نمی‌شه execution context مختص اون فانکشن ساخته بشه، بلکه باید اون فانکشن فراخوانی بشه تا execution context مختص بهش هم ساخته بشه.

2- مرحله دوم:

در این مرحله به محض اینکه فانکشن ()first فراخوانی شد، execution context مختص بهش هم ساخته می‌شه و کدهای این فانکشن داخل execution context ذخیره و اجرا می‌شن. الان execution context فعال، first execution context هست که در execution stack، بالای global execution context قرار می‌گیره.

ابتدا var b داخل execution context ذخیره می‌شه و بعد نوبت به خط بعدی کد می‌رسه. تو این خط از کد، فانکشن ()second فراخوانی می‌شه.


3- مرحله سوم:

خب فکر کنم دیگه همه می‌دونیم چه اتفاقی می‌افته؟

بله درسته، به محض اینکه فانکشن ()second فراخوانی می‌شه، مثل مرحله قبل یه execution context مختص به فانکشن ()second هم ساخته می‌شه و execution context فعال، second execution context می‌شه که در execution stack، بالای first execution context قرار می‌گیره.

در این مرحله هم var c داخل execution context ذخیره می‌شه و بعد نوبت به خط بعدی کد می‌رسه. تو این خط از کد، فانکشن ()third فراخوانی می‌شه.

4- مرحله چهارم:

در این مرحله هم مثل مراحل دوم و سوم بعد از فراخوانی فانکشن، execution context مختص به فانکشن ()third هم ساخته می‌شه که در این مرحله execution context فعال هم هست و بالاتر از بقیه execution context ها قرار می‌گیره و همچنین var d داخل اون ذخیره می‌شه.

5- مرحله پنجم:

بعد اینکه third execution context ساخته و کدهای داخل اون تفسیر شدن نوبت به برگشت داده شدن (return) این execution context و حذفش از execution stack هست.

این عمل برای دو function execution context دیگه و global execution stack هم تکرار می‌شه تا از execution stack حذف بشن. این همون مکانیزم LIFO هست که آخرین execution context که در execution stack قرار گرفته، اول از همه هم برگشت داده می‌شه و اولین execution context آخر از همه برگشت داده می‌شه .

پس به ترتیبی که در مراحل بالا دیدیم، موتور جاوااسکریپت کد ما رو اجرا و تفسیر کرد.



یاد آوری : execution context رو میشه به عنوان یک شئ (object) هم در نظر گرفت که ویژگی‌هایی (properties) هم داره.

خب، وقتشه execution context رو بیشتر بررسی کنیم و ببینیم execution context چجوری به وسیله‌ی موتور جاوااسکریپت ساخته می‌شه.

در جاوااسکریپت execution context یک مفهوم انتزاعی و فرضی است که اطلاعات (this keyword, objects variables, functions) قسمتی از کد که در حال تفسیر و اجرا توسط موتور جاوااسکریپت (javascript engine) است در آن ذخیره شده. به عبارت بهتر هر زمانی که کد در جاوااسکریپت اجرا می‌شه در واقع داخل یک execution context این اتفاق می‌افته.

**نکته مهم** در ورژن ES6 جاوااسکریپت، پراپرتی‌های execution context بر اساس دو مفهوم VariableEnvironment و LexicalEnvironment ارائه شدن. هر چند مفهوم کلی چندان تفاوتی نکرده. برای کسانی که می‌خوان با این مفاهیم آشنا بشن، به نظرم اگه ابتدا با مفاهیم قدیمی‌تر شروع کنن بهتره و کمتر سردرگم می‌شن.


Execution Context Object Properties:

1- variable object:

یک مفهوم انتزاعی که تمام اطلاعات execution context در اون ذخیره شده. منظور از اطلاعات، variables و function declarations و arguments هایی است که داخل execution context ذخیره شدن.

اگه از بخش اول یادتون باشه، برای اینکه مفهوم execution context رو بهتر درک کنیم، اون رو به یه جعبه تشبیه کردیم. حالا برای اینکه مفهوم variable object رو هم بهتر درک، اون رو هم به عنوان یه جعبه‌ی کوچک در نظر بگیرین که داخل execution context قرار گرفته و در واقع اطلاعات ذخیره شده در execution context در این جعبه و این قسمت از execution context ذخیره می‌شن.

خب الان بیاین کد زیر رو که در global execution context نوشته شده، بررسی کنیم:

var a = 100; 
var b = &quothello world"
function foo () {};


با توجه به گفته‌های بالا variable object ساخته شده برای global execution context شامل اطلاعات زیر می‌شه:

  • 1- var a
  • 2- var b
  • 3- foo ()


2- scope chain:

scope:

دسترسی به variable ها به وسیله scope تعین می‌شه. یعنی variable هایی که داخل یه scope تعریف شدن، خارج از اون قابل دسترس نیستن. scope باعث می‌شه variable ها فقط در داخل قسمت‌های مشخص شده‌ای از برنامه که به وسیله ما تعین شده قابل استفاده باشن.


در جاوااسکریپت 3 نوع scope داریم:

1- global scope:

همون طور که global execution context داریم، یه global scope هم داریم که به محض ایجاد global execution context، به صورت پیش‌فرض توسط موتور جاوااسکریپت ایجاد می‌شه و تمام scope هایی که بعدا ایجاد می‌شن scope درونی یا فرزند global scope هستن. هر variable که در global scope تعریف شده در همه جای برنامه قابل دسترس است.

var message= 'Hello World!';

function sayHello() {
   console.log(message); // return Hello World!
}

sayHello();


2- Local Scope or Function Scope:

با نوشتن، فراخوانی و ساخت فانکشن می‌شه یه scope جدید ساخت که به این نوع از scope ها Function Scope یا Local Scope گفته ‌می‌شه.

function sayHello() {
   var message= 'Hello World!';
   console.log(message); // return Hello World!
}

sayHello();
console.log(message); // return message is not defined

اگه کد بالا رو تو کنسول مرورگر اجرا کنیم، تو خط 3 کد، «Hello World!» برای ما برگشت داده می‌شه چون var message داخل فانکشن ()sayHello تعریف شده و فانکشن ()console.log خط 3 هم چون داخل این فانکشن و در نتیجه داخل این scope است پس بهش دسترسی پیدا می‌کنه و «Hello World!» برگشت داده می‌شه. ولی چون فانکشن ()console.log خط 7 خارج از scope و فانکشن ()sayHello نوشته شده بنابراین به var message دسترسی نداره.


3- block scope:

به محدوده‌ی بین دو براکت بلاک اسکوپ گفته می‌شه. (در let ،ES6 و const داخل { } قابل دسترس هستند و در واقع بین دو { }، یک اسکوپ ایجاد می‌شه.)

{
 var first = 'first message.';
 let second = 'second message';
 const third = 'third message';
} 

console.log(first); // return 'first message'. 
console.log(second); // return second is not defined 
console.log(third); // return third is not defined

با توجه به کد بالا به این نتیجه می‌رسیم که متغیر var بر خلاف let و const از نوع block scope نیست و خارج از { } هم قابل دسترسه.

تا اینجا با مفهوم scope آشنا شدیم ولی این همه‌ی مواردی که باید درباره‌ scope بدونیم نیست. الان وقتشه درباره‌ lexical scoping بحث کنیم.


lexical scope:

در جاوااسکریپت lexical scoping به این معنی و مفهوم است که scope های درونی به scope های بیرونی دسترسی دارند. یعنی اگه یه variable در یه scope تعریف شده، scope (های) درونی اون می‌تونن به اون variable دسترسی پیدا کنن. ولی scope (های) بیرونی نمی‌تونن به اون variable دسترسی پیدا کنن.

** یک نکته مهم که باید بهش توجه داشته باشیم اینه که scope ها مستقیما به variable (ها) دسترسی ندارن. در واقع scope ها به variable object ها دسترسی پیدا می‌کنن و چون variable ها داخل variable object ها ذخیره شدن، از این طریق دسترسی حاصل می‌شه.

بیایین کد زیر رو تو 3 مرحله بررسی کنیم:

var a = 'north London '; 

first(); 

function first () { 
    var b = 'is ';
    second(); 

    function second () {
        var c = 'red.';
        console.log(a + b + c); // return &quotnorth London is red.&quot
    }
}

1- مرحله اول:

یه global scope داریم و همون طور که قبلا گفته شد به محض ایجاد global execution context، به صورت پیش‌فرض توسط موتور جاوااسکریپت ایجاد می‌شه و تمام scope هایی که بعدا ایجاد می‌شن scope درونی یا فرزند global scope هستن.

یه scope توسط ()first ایجاد شده که scope درونی یا فرزند global scope است و یه scope هم توسط فانکشن () second ایجاد شده که scope درونی یا فرزند first() scope و global scope است.

2- مرحله دوم:

در var a ،global scope تعریف شده. var b داخل فانکشن ()first و var c داخل فانکشن () second تعریف شدن.

3- مرحله سوم:

درونی‌ترین scope در کد بالا second() scope است. پس این scope به variable object دو scope بیرونی دسترسی پیدا می‌کنه و چون var a و var b داخل variable object ها ذخیره شدن، این دسترسی حاصل می‌شه و مقدار «.north London is red» برگشت داده می‌شه. در حالی که اگه ()console.log داخل فانکشن ()first بود مقدار «c is not defined» برگشت داده می‌شد. و اگه داخل global scope بود ابتدا «b is not defined» و بعدا «c is not defined» برگشت داده می‌شد.


scope chain:

تو کد بالا دیدیم که فانکشن ()console.log حاصل جمع سه var رو بدون هیچ اروری برگشت داد و این یعنی فانکشن ()console.log به هر سه var دسترسی پیدا کرده. در واقع var c تو همون second() scope قرار داره و طبیعیه که بهش دسترسی داشته باشه، چون باهم تو یه scope قرار گرفتن. ولی او دو تا var دیگه تو اون scope نیستن، بنابراین موتور جاوااسکریپت با استفاده از ویژگی lexical scoping جاوااسکریپت به scope های بیرونی مراجعه می‌کنه تا شاید مقدار اون دو تا var رو پیدا کنه که در مرحله اول می‌رسه به first() scope. تو این scope هم var b تعریف شده و مقدار این متغیر پیدا می‌شه. ولی هنوز مقدار var a مشخص نشده، بنابراین در مرحله دوم موتور جاوااسکریپت یه مرحله جلوتر می‌ره و مراجعه می‌کنه به scope بیرونی‌تر و آخرین scope موجود که همون global scope هست. تو این scope هم var a مشخص شده و تمام variables مقدارشون مشخص می‌شن و کدی که نوشتیم بدون هیچ اروری نتیجه رو به ما برگشت می‌ده.

پس اگه یه variable تو scope فعلی مقدارش مشخص نشده باشه، موتور جاوااسکریپت تو scope های بیرونی‌تر دنبالش می‌گرده و این جستجو تا آخرین scope بیرونی که همون global scope است ادامه پیدا می‌کنه و اگه اونجا هم پیدا نشه برنامه ارور می‌ده. همه‌ی این اتفاق‌ها به لطف scope chain هست. scope chain جاییه که نه‌تنها variable object اون scope ذخیره شده بلکه یه راه ارتباطی هم وجود داره تا scope های درونی به variable object های scope های بیرونی دسترسی پیدا کنن. و این در حالیه که این ارتباط یه طرفست و فقط scope درونی می‌تونه به scope های بیرونی دسترسی داشته باشه.

* درواقع می‌شه scope chain یه execution context رو دربردانده variable object اون scope و execution context دونست که به variable objectهای scope ها و execution context های بیرونی دسترسی داره.

تا اینجا با 2 تا از پراپرتی‌های execution context آشنا شدیم ولی پراپرتی سوم رو تو بخش دوم مقاله باهم بررسی می‌کنیم


منابع:

www.freecodecamp.org

virgool.io/@ehsan_n