حمید تدینی
حمید تدینی
خواندن ۷ دقیقه·۴ سال پیش

هر چیزی که در رابطه با Scope توی جاوااسکریپت نیاز دارید بدونید

هر چیزی که در رابطه با Scope توی جاوااسکریپت نیاز دارید بدونید
هر چیزی که در رابطه با Scope توی جاوااسکریپت نیاز دارید بدونید


اگه روزهای اول کارتون با جاوا اسکریپت هستش و تجربه زیادی توی کار با این زبان ندارید ممکنه خیلی وقت ها براتون سوال پیش بیاد که چرا از bind/call/apply و یا از this استفاده می کنیم و چه زمان هایی باید از اونها استفاده کنیم. همچنین اگه قصد دارید تا از jQuery استفاده کنید یا یه کامپوننت اختصاصی بنویسید یا میخواید از Angular یا React و Vue و فریم ورک های دیگه استفاده کنید، این موارد میتونه خیلی بدردتون بخوره و باید مفهوم دقیق Scopeها و موارد استفاده اونها رو بدونید تا بتونید کدنویس خوبی توی جاوا اسکریپت بشید.

تعریف Lexical Scoping

وقتی توی سایت های آموزشی مختلف می چرخید، با Lexical Scoping خیلی مواجه می شید اما در واقع معنی این واژه چیه؟

در واقع Lexical Scoping یک الگوی طراحی هستش که توی این مدل یک متغیر فقط قابل استفاده توی همون بلاکی هستش که توی اون تعریف شده. اینطوری میشه گفت که هر بلوکی که تعریف شده Scope مخصوص به خودش رو داره.

توی جاوا اسکریپت ما ترکیبی از Lexical Scoping رو به همراه function scope در اختیار داریم. بنابراین، توی جاوا اسکریپت بجای اینکه یه Scope برای هر بلاک ایجاد بشه، در واقع یه scope جدید برای هر function تعریف می شه.

به مثال زیر دقت کنید. اگه شما این قطعه کد رو توی console مرورگر خودتون اجرا کنید، نتیجه ای که توی خروجی به شما داده میشه، عدد 1 هستش. اما واقعا browser چطور این کدها رو تفسیر و اجرا کرده؟

var a = 1; function firstFunc(){ console.log(a); } function secondFunc(){ var a = 5; return firstFunc(); } secondFunc();

در واقع firstFunc و secondFunc هر دو توابعی هستند که جاوا اسکریپت برای اونها یک scope جداگانه تعریف کرده. با این شرایط هر کدوم از این توابع یه scope منحصربفرد دارن و جاوا اسکریپت اول توی scope اونها دنبال متغیر موردنظر میگرده و بعد به بالاتر میره و توی بلوک بالاتر دنبالش میگرده تا به بلوک کلی اولیه برسه.

اگه بخوایم ماجرا رو بیشتر باز کنیم، این اتفاق افتاده که ما توی خط 1 اومدیم و یه متغیر a با استفاده از کلمه کلیدی var تعریف کردیم که توی بلوک اصلی تعریف شده. بعد از اون توی خط 6 یه متغیر دیگه به نام a تعریف کردیم که با اینکه اون رو هم با کلمه کلیدی var تعریف کردیم ولی جایگزین مقدار تعریف شده توی خط 1 نشده و با اجرای خط 3 ما نتیجه 1 رو مشاهده کردیم!

دلیل این مسئله اینه که همونطور که گفتم هر کدوم از این توابع یه scope برای خودشون دارن که شامل scope مربوط به بلوک بالاتر هم میشه. بخاطر همین توی firstFunc، وقتی که میخوایم متغیر a رو چاپ کنیم بهمون نمیگه که این متغیر موجود نیست و میره از بلوک بالاتر اون رو میخونه ولی اگه توی secondFunc از دستور console.log() استفاده می کردیم برای ما مقدار 5 رو چاپ میکرد چون اول توی خود بلوک دنبال اون متغیر میگرده و اگه پیداش نکنه میره توی بلوک بالاتر و بالاتر دنبالش بگرده.

کلمه کلیدی this

در صورتی که با this آشنایی ندارید، عملکرد این کلمه کلیدی براتون کمی عجیب به نظر میاد. this بصورت پیشفرض به اشیا موجود در function اشاره میکنه. بنابراین مصلا اگر یه function رو تعریف کنید، this به اشیا داخل اون function اشاره میکنه. اما اگه توی هر جای دیگه ای از کد استفاده کنید، کلمه کلیدی this به شئ اصلی صفحه یا همون window اشاره میکنه.

یه نگاهی به کد زیر بندازین:

var person1 = { &quotname&quot: &quotHamid&quot, &quotage&quot: 30, &quotcountry&quot: &quotIran&quot, getDetails: function(city, occup){ console.log(&quotouter function&quot); console.log(&quotname: &quot + this.name + &quot, age: &quot + this.age + &quot , country: &quot + this.country); innerFunction = function() { console.log(&quotinner function&quot); console.log(&quotname: &quot + this.name + &quot, age: &quot + this.age + &quot , country: &quot + this.country); } innerFunction(); } } person1.getDetails();

این هم خروجی مربوط به کد بالا هستش:

outer function
name: Hamid, age: 30 , country: Iran
inner function
name: , age: undefined , country: undefined

بیاید ببینیم دقیقا چه اتفاقی افتاد. در خط ۵ تابع getDetails بعنوان یک آبجکت برای person تعریف شده که در واقع scope اون شامل موارد موجود توی person میشه. پس this توی اینجا به آیتم های توی person اشاره می کنه. زمانی که متد innerFunction() رو ساختیم، یه scope جدید ساخته میشه که به global window اشاره می کنه.

پس اگه ما توی scope اصلی بیایم و مقادیر رو تغییر بدیم، باید منتظر ایجاد تغییر توی مقادیر مربوط به person هم باشیم. بیاید کدهای زیر رو اضافه کنید تا نتیجه رو مشاهده کنیم:

var person1 = { &quotname&quot: &quotHamid&quot, &quotage&quot: 30, &quotcountry&quot: &quotIran&quot, getDetails: function(city, occup){ console.log(&quotouter function&quot); console.log(&quotname: &quot + this.name + &quot, age: &quot + this.age + &quot , country: &quot + this.country); innerFunction = function() { console.log(&quotinner function&quot); console.log(&quotname: &quot + this.name + &quot, age: &quot + this.age + &quot , country: &quot + this.country); } innerFunction(); } } person1.getDetails(); var name = &quotAlex&quot, country = &quotGermany&quot, age = 26; person1.getDetails();

خروجی که باید مشاهده کنید به این شکل هستش:

outer function
name: Hamid, age: 30 , country: Iran
inner function
name: Alex, age: 26, country: Germany
outer function
name: Hamid, age: 30 , country: Iran
inner function
name: Alex, age: 26 , country: Germany

همونطور که گفتیم this توی innerFunction به scope جهانی یا global اشاره می کنه و مقادیر مربوط به اون رو نشون میده.

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

دستورات Bind / Call / Apply

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

قطعه کد زیر رو به دقت نگاه کنید:

var person1 = { &quotname&quot: &quotHamid&quot, &quotage&quot: 30, &quotcountry&quot: &quotIran&quot, getDetails: function(){ console.log(&quotname: &quot + this.name + &quot, age: &quot + this.age + &quot , country: &quot + this.country); } } var person2 = { &quotname&quot: &quotAlex&quot, &quotage&quot: 30, &quotcountry&quot: &quotGermany&quot, } person1.getDetails.bind(person2)();

خروجی مربوط به این کد مشابه زیر هستش:

name: Alex, age: 30 , country: Germany

همونطور که می بینید ما person1 رو فراخوانی کردیم ولی با استفاده از bind مقادیر person2 رو به اون متصل کردیم و خروجی ما با اینکه توی getDetails مربوط به person1 بود اما مقادیر جاگذاری شد و مقادیر جدید نمایش داده شد.

تمامی متدهای Bind / Call / Apply برای همین منظور توی جاوااسکریپت استفاده می شوند.

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

علاوه بر این Apply و Call در موارد دیگری استفاده میشن. کد زیر رو ببینید:

var person1 = { &quotname&quot: &quotHamid&quot, &quotage&quot: 30, &quotcountry&quot: &quotIran&quot, getDetails: function(city, occup){ console.log(&quotname: &quot + this.name + &quot, age: &quot + this.age + &quot , country: &quot + this.country + &quot , city: &quot + city + &quot , occupation: &quot + occup); } } var person2 = { &quotname&quot: &quotAlex&quot, &quotage&quot: 30, &quotcountry&quot: &quotGermany&quot, } person1.getDetails.call(person2, &quotBangalore&quot, &quotMusician&quot); person1.getDetails.apply(person2, [&quotBangalore&quot, &quotMusician&quot]);

توی این مثال ما دو آرگومان جدید رو در هنگام فراخوانی getDetails ارسال کردیم. با استفاده از این روش ها می توانید همزمان مقادیر مربوط به آرگومان ها رو هم ارسال کنید.

در خط 14 مشاهده می کنید که با استفاده از Call، آرگومان ها بصورت جداگانه ارسال میشن ولی توی خط 15 و با استفاده از Apply، باید آرگومان های مربوطه رو بصورت یک آرایه ارسال کردیم.

مفهوم Closureها

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

اگه بخوام خیلی ساده بگم، در واقع Closure یک ترکیب از یک function و scope اطراف اون هستش. توی جاوااسکریپت هر زمانی که شما یک function رو ایجاد کنید، یک closure ایجاد میشه که به شما امکان دسترسی به scopeهای دیگه رو به همراه global scope میده.

کد زیر رو توی مرورگر تست کنید تا نتیجه رو بهتر متوجه بشید:

var name1 = &quotglobal&quot function outerFn() { console.log(name1); var name2 = &quotouter function&quot function innerFn(){ console.log(name1); console.log(name2); var name3 = &quotinner function&quot } innerFn(); console.dir(innerFn); } outerFn(); console.dir(outerFn);

از دستور console.dir برای شبیه سازی یک function و خروجی گرفتن از تمامی proprtieهای اون استفاده می کنیم. خروجی کد بالا مشابه این هستش:

همونطور که مشاهده می کنید بطور کامل واضح، مشخصه که innerFn دسترسی به name1 هم داره همچنین innerFn دسترسی به outerFn و متغیر name2 هم داره. توی console خودتون innerFn و outerFn رو باز کنید تا ببینید دقیقا به چه Scopeهایی دسترسی دارن.

تصویری از دسترسی های outerFn
تصویری از دسترسی های outerFn


تصویری از دسترسی های innerFn
تصویری از دسترسی های innerFn


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

این مقاله برگرفته از مقاله آقای Akarshan Bansal هستش.

جاوا اسکریپتjavascriptscopeبرنامه نویسیClosure
برنامه نویس و فعال حوزه هوش مصنوعی
شاید از این پست‌ها خوشتان بیاید