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

جاوااسکریپت×تو بخش اول مقاله به ترتیب درباره execution context و دو تا از پراپرتی‌هاش صحبت کردیم و قرار شد تو بخش دوم درباره پراپرتی سوم که this هست بحث کنیم تا برسیم به چگونگی ایجاد execution context.
×وقتی execution context ساخته می‌شه this هم مثل scope chain و variable object باهاش همراه و تو execution context ذخیره می‍شه تا مثل اون دو تا یه سری امکانات در اختیار ما قرار بده.

this keyword:

به صورت خلاصه this به یک object اشاره داره. به عبارت بهتر this به آبجکتی اشاره داره که فانکشن در حال اجرای فعلی رو اجرا (execute) می‌کنه. (به فارسی سخت!)

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

اگه یادتون باشه تو بخش اول گفتیم که:

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

و همین‌طور گفته شد:

اگر execution context رو به عنوان object درنظر بگیریم، global execution context هم به عنوان global object شناخته می‌شه.

با توجه به تعریفی که از this نوشتیم و این دو یادآوری، this درواقع به یک execution context اشاره داره.

پس در موقعیت‌های مختلف، this به آبجکت‌ها و execution context های مختلفی اشاره می‌کنه:

1- this & Window (window binding):

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

اگه this رو در global scope استفاده کنیم، به global execution context یا همون global object اشاره داره. اگه بخش اول مقاله یادتون باشه، گفتیم که:

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


2- this & regular Function (Implicit binding):

var name = 'Milan'; 

function footballClub () {
    var name = 'Arsenal';
    console.log(this); // return window object
    console.log(this.name); // return Milan
}

footballClub();

وقتی this داخل یه فانکشن نوشته می‌شه به global execution context یا همون global object اشاره داره!

به همین دلیل this موجود در فانکشن ()footballClub به gloabl object اشاره می‌کنه و this.name داخل فانکشن به name موجود در global object.


3- this & Method (Implicit binding):

var name = 'Milan';

var footballClub = {
    name : 'Arsenal',
    clubDetail : function () {
        console.log(this); // return footballClub object
        console.log(this.name); // return Arsenal
    }
}

footballClub.clubDetail();

برخلاف فانکشن‌ها وقتی this داخل یه متد بکار می‌ره، اشارش به اون آبجکتی هست که داخلش تعریف شده و (آبجکت) اون method رو فراخوانی می‌کنه.

اشاره this به آبجکت footballClub هست. چون this داخل متدی که تو این آبجکت به‌کار رفته، استفاده شده. this.name هم به name موجود در footballClub object اشاره می‌کنه نه name موجود در global object.

* یه نکته که باید به اون توجه داشته باشیم اینه که اگه یه فانکشن داخل یه متدی بکار رفته باشه، this موجود در اون فانکش باز هم به global object اشاره خواهد داشت.


4- this & new (new binding):

function Person (fullName, yearOfBirth, job) {
    this.fullName = fullName;
    this.yearOfBirth = yearOfBirth;
    this.job = job;
    console.log(this) // retun Elon object and Jeffrey object
}

var Elon = new Person('Elon Reeve Musk', 1971, 'CEO');
var Jeffrey = new Person('Jeffrey Preston Bezos', 1964, 'CEO');

در جاوااسکریپت یکی از راه‌های ساخت object، استفاده از function constructor هست. تو این روش ابتدا یه فانکشن به عنوان نمونه اولیه ایجاد می‌شه و بعدا آبجکت‌ها به عنوان نمونه‌‌های کپی شده از این function constructor ساخته می‌شن.

خب بیاین کد بالا رو یکم دقیق‌تر بررسی کنیم:

1- در مرحله اول فانکشن ()Person رو که یه function constructor هست رو می‌سازیم که 3 پارامتر داره و به عنوان نمونه اولیه از آبجکت‌هایی که بعدا از روی اون ساخته می‌شن شناخته می‌شه.

2- تو این مرحله با استفاده از new و فانکشن ()Person، آبجکت Elon رو به وجود میاریم. به این صورت که:

ابتدا new یک آبجکت خالی رو می‌سازه. بعد از new، فانکشن ()Person فراخوانی می‌شه که 3 پارامتر هم داره. پس آبجکت جدید (با استفاده از پارامترهای وارد شده) ساخته و در داخل متغیر Elon ذخیره می‌شه.

3- در این مرحله هم آبجکت Jeffrey مثل مرحله قبل ساخته می‌شه.

کاری که new در این موقعیت انجام می‌ده در واقع اینه که اشاره this رو از window object (چون اینجا this داخل یه فانکشن بکار رفته) به آبجکتی که تازه ساخته شده (Elon و Jeffrey) منتقل کنه. یعنی اگه کد بالا رو به صورت زیر که بدون بکار بردن new هست بنویسیم، this به window object اشاره می‌کنه:

function Person (fullName, yearOfBirth, job) {
    this.fullName = fullName;
    this.yearOfBirth = yearOfBirth;
    this.job = job;
    console.log(this) // return Window object twice
}

var Elon = Person('Elon Reeve Musk', 1971, 'CEO');
var Jeffrey = Person('Jeffrey Preston Bezos', 1964, 'CEO');


5- call(), apply() and bind() (Explicit binding):

call():

var name = 'Adam Wathan';
var job = 'back-end developer';

var developer = function () {
    console.log('My name is ' + this.name + ', I am ' + this.job);
} 

var rachel = {
    name: 'Rachel Andrew',
    job: 'front-end developer'
} 

developer(); // return &quotMy name is Adam Wathan, I am back-end developer&quot
developer.call(rachel); // return &quotMy name is Rachel Andrew, I am front-end developer&quot

در مثال بالا یه فانکشن داریم به اسم ()developer. بار اولی که این فانکشن فراخوانی شده (خط 13)، مقدار «My name is Adam Wathan, I am back-end developer» برگشت داده می‌شه. چون this داخل یه فانکشن بکار رفته و به همین دلیل شاره اون به window object هست.

ولی بار دومی که این فانکشن فراخوانی شده همراه با متد ()call بوده. تو این مثال متد ()call همون نقش new رو در مثال قبلی بازی می‌کنه. یعنی اشاره this رو از window object برمی‌داره و به آبجکتی که ما می‌خوایم (آبجکت rachel) منتقل می‌کنه. و این بار مقدار «My name is Rachel Andrew, I am front-end developer» برگشت داده می‌شه.

شاید سوال پیش بیاد که چرا اینجوری کد رو نوشتیم و چرا از همون اول فانکشن ()developer رو به عنوان متد داخل خود آبجکت بکار نبردیم؟

فرض کنین به جای یک آبجکت در مثال بالا، هزار آبجکت داشتیم و فانکشن ()developer هم به جای اینکه یک خط بود، هزار خط کد داشت. اگر می‌خاستیم این فانکشن هزار خطی رو به عنوان متد به هر یک از این هزار آبجکت اضافه کنیم، حافظه برنامه خیلی زود پر می‌شد و یا برنامه با کندی عمل می‌کرد. ولی با استفاده از متد ()call هر هزار آبجکت می‌تونن از این فانکشن بدون اشغال بیش از حد حافظه استفاده کنن.

یه نکته که باید در مورد متد call بدونیم این هست که می‌تونیم بیش از یک آرگومان به این متد وارد کنیم.

مثال:

var developer = function (skill1, skill2, skill3) {
    console.log('My name is ' + this.name + ', I am ' + this.job + '. I know ' + skill1 + ', ' + skill2 + ', ' + skill3);
} 

var rachel = { 
    name: 'Rachel Andrew',
    job: 'front-end developer'
}

developer.call(rachel, 'HTML', 'CSS', 'Javascript'); // return &quotMy name is Rachel Andrew, I am front-end developer. I know HTML, CSS, Javascript&quot


apply():

عملکرد متد ()aplly مشابه متد ()call هست، با این تفاوت که به این متد فقط دو آرگومان می‌تونیم وارد کنیم که آرگومان دوم باید به صورت آرایه باشه.

var developer = function (skill1, skill2, skill3) {
    console.log('My name is ' + this.name + ', I am ' + this.job + '. I know ' + skill1 + ', ' + skill2 + ', ' + skill3);
}

var rachel = { 
    name: 'Rachel Andrew',
    job: 'front-end developer'
}

var skills = ['HTML', 'CSS', 'Javascript'];
developer.apply(rachel, skills); // return &quotMy name is Rachel Andrew, I am front-end developer. I know HTML, CSS, Javascript&quot

همون طور که می‌بینید، عملکرد این متد شبیه متد ()call هست و دقیقا همون مقدار رو برگشت داده. فقط به جای اینکه «HTML»، «CSS» و «Javascript» رو به صورت آرگومان‌های جدا به متد اضافه کنیم، اون‌ها رو تو یه آرایه ذخیره و اون آرایه رو به عنوان آرگومان دوم به متد وارد کردیم.


bind():

متد ()bind هم با کمی تفاوت مثل متد ()call عمل می‌کنه:

var developer = function (skill1, skill2, skill3) {
    console.log('My name is ' + this.name + ', I am ' + this.job + '. I know ' + skill1 + ', ' + skill2 + ', ' + skill3);
}

var rachel = {
    name: 'Rachel Andrew',
    job: 'front-end developer'
}

var developerRachel = developer.bind(rachel, 'HTML', 'CSS', 'Javascript');
developerRachel(); // return &quotMy name is Rachel Andrew, I am front-end developer. I know HTML, CSS, Javascript&quot

تفاوت متد ()bind در این هست که بلافاصله فانکشن مورد نظر (فانکشن ()developer) رو فراخوانی و اجرا نمی‌کنه، بلکه یک نمونه کپی شده از اون تابع رو تولید می‌کنه و برگشت می‌ده و ما می‌تونیم با ذخیره اون در یک متغیر (متغیر developerRachel) در مواقع نیاز، اون فانکشن رو فراخوانی و اجرا کنیم.




تا اینجا درباره‌ی سه ویژگی execution context بحث کردیم و الان هر سه ویژگی رو به صورت کامل می‌شناسیم.از این به بعد درباره‌ی چگونگی ساخته شدن execution context بحث می‌کنیم.

موتور جاوااسکریپت execution context رو در دو مرحله ساخته و اجرا می‌کنه:

قبل از اجرای کد، یک مرحله داریم به اسم creation phase.

1- creation phase:

می‌شود گفت که Variable Object (VO) یک ظرف شی مانند است که در یک Execution Context ایجاد می شود. این متغیرها و function declarations تعریف شده را در آن Execution Context ذخیره می کند.

تو اولین قدم از این مرحله، variable object ساخته می‌شه. به این صورت که:

در صورتی که سازنده‌ی execution context یک فانکشن باشه، ابتدا آرگومان‌های فانکشن (در صورت وجود) داخل argument object ذخیره می‌شه (خود argument object بعد از ساخته شدن VO، داخل اون ذخیره می‌شه).

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

بعد اینکه تکلیف فانکشن‌های فراخوانی شده‌ی کد روشن شد، نوبت به متغیرهایی که با کلمه کلیدی var تعریف شدن می‌رسه. در این مرحله متغیرهایی که فراخوانی شدن داخل VO ذخیره می‌شن و مقدارشون برابر با undefined قرار داده می‌شه. متغیر حتی اگه مقداردهی هم شده باشه، تو این مرحله مقدارش برابر با undefined هست.

با توجه به گفته‌های دو پاراگراف آخر، چون فانکشن‌ها (declaration functions) و متغیرها در مرحله‌ی creation در VO ذخیره می‌شن، قبل از اجرای کد قابل دسترس هستند و در اصطلاح گفته می‌شه که hoisted شدن و به این عمل hoisting گفته می‌شه.

به همین دلیل هست که ما می‌تونیم از declaration functions قبل از فراخوانی و از متغیرها قبل از مقداردهی استفاده کنیم. هر چند در صورت اینکه اگر از متغیرها قبل مقداردهی استفاده کنیم، مقدار برگشتی برابر با undefined خواهد بود.

در دومین قدم scope chain اون execution context ساخته می‌شه.

و در سومین قدم مشخص می‌شه که this اون execution context به چه آبجکتی اشاره می‌کنه.


2- execution phase:

میرسیم به Execution Stack که با نام Call Stack نیز شناخته می شود، که تمام زمینه های اجرایی ایجاد شده در طول چرخه حیات یک اسکریپت را ردیابی می کند.

جاوا اسکریپت یک زبان تک رشته ای (single-threaded) است، به این معنی که تنها قادر به اجرای یک کار در یک زمان است. بنابراین، هنگامی که actions، functions و events دیگر رخ می دهد، یک Execution Context برای هر یک از این رویدادها ایجاد می شود. با توجه به ماهیت تک رشته ای جاوا اسکریپت، stack ای از زمینه های اجرایی انباشته شده برای اجرا ایجاد می شود که به عنوان Execution Stack شناخته می شود.

هنگامی که اسکریپت ها در مرورگر بارگذاری می شوند، Global context به عنوان default context ایجاد می شود که در آن موتور JS شروع به اجرای کد می کند و در پایین execution stack قرار می گیرد.

موتور JS سپس فراخوانی های تابع را در کد جستجو می کند. برای هر فراخوانی تابع، یک FEC جدید برای آن تابع ایجاد می شود و در بالای Execution Context فعلی قرار می گیرد.

همچنین Execution Context در بالای Execution stack به Execution Context فعال تبدیل می شود و همیشه ابتدا توسط موتور JS اجرا می شود.

به محض اینکه اجرای همه کدها در Execution Context فعال انجام شد، موتور JS جاوااسکریپت Execution Context آن تابع خاص از execution stack را بیرون می‌آورد، به سمت بعدی زیر آن حرکت می‌کند و غیره.

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

برای درک فرآیند کار execution stack، به مثال کد زیر توجه کنید:

var name = &quotVictor"

function first() {
  var a = &quotHi!"
  second();
  console.log(`${a} ${name}`);
}

function second() {
  var b = &quotHey!"
  third();
  console.log(`${b} ${name}`);
}

function third() {
  var c = &quotHello!"
  console.log(`${c} ${name}`);
}

first();

ابتدا اسکریپت در موتور JS بارگذاری می شود.

پس از آن، موتور GEC ،JS را ایجاد می کند و آن را در پایه execution stack قرار می دهد.

متغیرnameخارج از هر تابعی تعریف شده است، بنابراین در GEC است و در VO آن ذخیره می شود.

همین فرآیند برای توابع اول، دوم و سوم اتفاق می افتد.

در مورد اینکه چرا آنها هنوز در GEC هستند، گیج نشوید. به یاد داشته باشید که GEC فقط برای کدهای جاوا اسکریپت (متغیرها و توابع) است که داخل هیچ تابعی نیستند. از آنجایی که آنها در هیچ تابعی تعریف نشده اند، اعلان های تابع در GEC هستند. الان متوجه شدی عزیزم ?؟

هنگامی که موتور JS با اولین فراخوانی تابع یا function call روبرو می شود، یک FEC جدید برای آن ایجاد می شود. این context جدید در بالای context فعلی قرار می گیرد و به اصطلاح Execution Stack را تشکیل می دهد.

در طول مدت اولین فراخوانی تابع، Execution Context آن به active context تبدیل می شود که ابتدا کد جاوا اسکریپت در آنجا اجرا یا executed می شود.

در تابعfirstمتغیر '!a = 'Hi در FEC خود ذخیره می شود، نه در GEC.

سپس تابعsecondدر تابعfirstفراخوانی می شود.

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

دوباره موتور JS یک FEC جدید را برای تابعsecondتنظیم می کند و آن را در بالای stack قرار می دهد و آن را به active context تبدیل می کند.

تابعsecondبه active context تبدیل می شود، متغیر ;'!b = 'Hey در FEC خود ذخیره می شود و تابعthirdدر تابعsecondفراخوانی می شود. FEC آن ایجاد می شود و در بالای execution stack قرار می گیرد.

در داخل تابعthirdمتغیر'!c = 'Hello در FEC آن ذخیره می شود و پیام Hello! Victor وارد کنسول می شود.

از این رو تابع تمام وظایف خود را انجام داده است و می گوییمreturnsشود. FEC آن از بالای stack حذف می شود و FEC تابعsecondکه تابعthirdنامیده می شود به active context باز می گردد.

هنگامی که اولین تابع به طور کامل اجرا می شود، execution stack اولین تابع از stack خارج می شود(popped out). بنابراین، کنترل به GEC کد باز می گردد.

و در نهایت، زمانی که اجرای کل کد کامل شد، موتور JS جاوااسکریپت GEC را از current stack حذف می کند.


منابع:

www.freecodecamp.org

virgool.io/@ehsan_n