Sam Aghapour
Sam Aghapour
خواندن ۱۲ دقیقه·۳ سال پیش

یادگیری hoisting , scopes , closures در جاوا اسکریپت

سلام به جی اس کارای عزیز ?

توی جاوا اسکریپت یه سری مباحث اولیه اما خیلی مهم هستن که یادگیریشون برای هر جی اس دولوپری واجبه که امروز قراره چندتا از اون مباحث شاملِ scopes , closures , hoisting رو زیر و رو کنیم و ببینیم چی هستن و چیکار میکنن و به چه دردی میخورن اما این زیر و رو کردن رو سعی میکنم به ساده ترین و کوتاه ترین شکل ممکن انجام بدم که نه گیج بشید نه حوصلتون سر بره..


1. بحث Scopes **

اسکوپ یا محدوده ینی چی؟
اسکوپ، محدوده ای هستش که یه متغیر یا فانکشن یا ابجکت در دسترس و قابل استفاده هستش به همین سادگی

اسکوپ ها در جاوااسکریپت ؟

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

  1. محدوده جهانی یا Global Scope
  2. محدوده محلی یا Local Scope

متغیر هایی که داخل فانکشن ساخته میشن داخلِ local scope هستن و متغیر هایی که خارج از فانکشن ها ساخته میشن داخلِ global scope هستن

محدوده جهانی یا Global Scope

وقتی شروع به ساختن یه متغیر توی فایل جاوااسکریپت میکنی که داخل هیچ فانکشنی نیست شما این فانکشن رو داخل محدوده Global نوشتی و میتونی هر جایی حتی داخل local scope ها (داخل فانکشن ها) بهشون دسترسی داشته باشی :

var someThing = 'sam' console.log(someThing); //output: 'sam' function callSomeThing () { console.log(someThing); //output: 'sam' //متغیری که تو محدوده جهانی نوشتیم اینجا هم در دسترس هستش }


محدوده محلی یا Local Scope

متغیر هایی که داخل فانکشن ها تعریف میکنیم داخل محدوده محلی یا local scope هستن و محدود به محدوده همون فانکشن هستن و نه هیچ جای دیگه(ینی نه داخل فانکشن های دیگه و نه داخل محدوده جهانی ، که البته یه استثنای کوچیکی هست که تو جلوتر بهش میرسیم)

//Global Scope function someFunc() { // Local scope 1 اولین محدوده محلی functoin someOtherFun(){ //local scope 2 دومین محدوده محلی } } function thirdFunc(){ //local scope 3 سومین محدوده محلی }


منطقه محدود Block statement

این وسط لازم به ذکره که محدوده های حلقه های for , while و یا محدوده های if , switch برخلاف فانکشن ها محدوده خاص محلی ندارن و از محدوده جایی که توش هستند استفاده میکنن، ینی چی؟

ینی اینکه اگه یه شرط یا حلقه توی global scope نوشته شده باشه متغیرای داخلشم توی global scope به حساب میان و همه جا در دسترس اند. مثل:

if(condition){ // global scope این شرط در محدوده جهانی نوشته شده var someThing = 'sam'; // this is still in global scope } console.log(someThing); //output : 'sam'


توی ES6 ، دو نوع متغیر جدید به اسم های let , const معرفی شدند که یه فرقایی نسبت به var دارند و یکی از اون فرق ها همینجا خودشو نشون میده اونم اینه که let و const برخلاف var وقتی داخل محدوده حلقه یا شرط استفاده بشن اون متغیر داخل همون محدوده قابل استفاده است و نه خارج از اون! مثل:

if(condition){ // someThing is in the global scope because of the 'var' keyword var someThing = 'sam' ; // secondThing is in the local scope because of the 'let' keyword let secondThing = 'javaScript is great!' ; // thirdThing is in the local scope because of the 'let' keyword const thirdThing= 2022 ; } console.log(someThing); //output : 'sam' console.log(secondThing); //output : Uncaught ReferenceError: secondThing is not defined console.log(thirdThing); //output : Uncaught ReferenceError: thirdThing is not defined


بحث Lexical scope

بالاتر گفتم که " متغیر هایی که داخل فانکشن ها تعریف میکنیم داخل local scope هستن و محدود به محدوده همون فانکشن هستن و نه هیچ جای دیگه که البته یه استثنای کوچیکی هست که تو جلوتر بهش میرسیم)"

خب اینجا همونجاییه که باید بگم موقع استفاده از متغیر داخل فانکشن یا local scope بحثی وجود داره به اسم lexical scope ( بعضیا static scope هم میگن بهش). حالا این ینی چی؟

این موضوع تو خیلی از زبونای برنامه نویسی وجود داره و اگه خیلی ساده بخوام بگم به این معنیه که اگه ما یه فانکشن داخل یه فانکشن دیگه داشته باشیم میتونیم به متغیرای محدوده ی محلیِ فانکشن پدرش دسترسی داشته باشه و به بیان دیگه هرچند تا فانکشن تو در تو داشته باشیم فانشکن فرزند علاوه بر متغیرای محدوده خودش به متغیرای محدوده پدرش و پدرِ پدرش و... دسترسی داره البته این رو در نظر بگیرید که این رابطه از بالا به پایینه و بر خلاف فانکشن فرزند ، فانکشن پدر نمیتونه به متغیرای محدوده فانکشن فرزند دسترسی داشته باشه! مثالش:

lexical scope
lexical scope

توی عکس بالا همونطور که از کامنتا مشخصه فانکشن بالاتر نمیتونه به متغیرای فانکشنای داخلش دسترسی داشته باشه اما فانکشن های فرزند میتونن به متغیرای فانکشنی که توش هستن دسترسی داشته باشن.


بحث dynamic scope

حالا که بحث static scope رو گفتیم بزارید یه نگاه کوچیکی هم داشته باشیم به dynamic scope تا کاملش کنیم. مثال زیر رو نگاه کنید تا فرق بین dynamic scope و lexical scope رو متوجه شیم.

lexical scope vs dynamic scope
lexical scope vs dynamic scope

توی بحث lexical scope اگه کد بالارو اجرا کنیم وقتی فانکشن b صدا زده میشه داخلش فانکشن a اجرا شده و فانکشن a داخلش someVar رو لاگ کرده خب وقتی توی a هیچ متغیری با اسم someVar نداریم بخاطر lexical بودن میاد اون محدوده ای که این فانکشن توش تعریف شده رو میگرده (پرنتش) تا ببینه میتونه این متغیر رو اونجا پیدا کنه و بله! متغیر someVar توی محدوده ای که فانکشن a تعریف شده وجود داره و برابر با 0 هستش پس خروجی نهایی 0 هستش.
حالا اگه بحث dynamic scope رو در نظر بگیریم توی کد بالا فانکشن a وقتی میبینه متغیر someVar وجود نداره بجای محدوده ی جایی که توش تعریف شده میره محدوده ای که توش "صدا زده شده" رو میگرده ینی محدوده محلیِ فانکشن b که متغیر someVar برابر با 1 هستش و خروجی نهایی 1 هستش.

نتیجه نهایی از مقایسه:

  1. توی lexical scope توی محدوده ای دنبال متغیر میگرده که فانکشن توش تعریف شده
  2. توی dynamic scope توی محدوده ای دنبال متغیر میگرده که فانکشن توش صدا زده شده

بحث scopes اینحا تموم میشه و حالا میریم سراغ closure


2. بحث closures **

قابلیت closure توی جاوااسکریپت این امکان رو به فانکشن های داخل یا فانکشن دیگه میده که بتونه به متغیرای محدوده فانکشنی که توش تعریف شده دسترسی داشته باشه که تاالان با این قضیه تا حدی آشنا شدیم اما کاربرد و جالبیش هنوز مونده!

پس closure یه فانکشن داخلی هستش که به سه تا متغیرای سه نوع محدوده (scope-chain)دسترسی داره :

  1. محدوده خودش
  2. محدوده فانکشن هایی که توش ساخته شده
  3. محدوده جهانی یا global scope
function outer() { var b = 50; function inner() { var a = 30; console.log(a+b); } return inner } let x = outer();

خب توی کد بالا میتونید حدس بزنید closure ما کدومه؟
درسته! inner فانکشن closure ما هستش که داخل یه فانکشن outer صدا زده شده و به مقدار b داخل محدوده outer هم دسترسی داره

یه چیزی رو به خاطر داشته باشید که متغیرای داخل یه فانکشن تا زمانی وجود دارن که اون فانکشن داره اجرا میشه و به محض اینکه فانکشن اجرا و تموم بشه اون متغیر داخلش دیگه وجود نداره تا زمانی که دوباره فانکشن صدا زده بشه و اجرا شه حالا با توجه به این توضیحات بیاین این کد بالارو کامل قدم به قدم کالبد شکافی کنیم

  1. فانکشن outer شامل یه متغیر b با مقدار 50 هستش و در آخر داره فانکشن inner رو برمیگردونه
  2. دقت داشته باشید که داره خود فانکشن رو برمیگردونه نه مقادیر داخل فانکشن inner رو چون inner رو بدون پرانتز برگردونده
  3. پس وقتی ما متغیرx رو برابر با ()outer قرار بدیم x برابر میشه با فانکشن inner و الان x یه فانکشن هست
x = function inner () { var a = 30; console.log(a+b); }

4. فانکشنouter وقتی ریختیمش تویx اجرا شد و تموم شد و حالا مقدار b دیگه وجود نداره! پس وقتی x رو صدا بزنیم دقیقا b رو از کجا میاره که با متغیرa جمع بزنه و نمایش بده ؟؟

5. اینجاست که داستان جالب میشه! جاوااسکریپت متغیر a رو میبینه که برابر با 30 هستش و اوکیه و وقتی میاد میرسه به console.log میبینه یه b هم اینجا هستش! حالا تنهاقابلیتی که اینجا به داد جاوااسکریپت میرسه چیه؟؟؟ آفرین closure

6. فانکشن inner میره سراغ محدوده فانکشنی که توش ساخته شده و اونجا دنبال b میگرده و پیداش میکنه و درنتیجه وقتی x رو اجرا کنیم a و b رو جمع میزنه و 80 رو نمایش میده


بیاین برای اینکه مطمعن شیم این مبحث رو دونستیم و کاربرد اصلیو فهمیدیم یه مثال کاربردی تر دیگه رو قدم به قدم کالبدشکافی کنیم:

function NumberFunc() { let i = 0; function incrementNumberFunc() { let b = 10 console.log(`i = ${i++} , b = ${b++}`); } return incrementNumberFunc } let x = NumberFunc(); let y = NumberFunc(); x(); //output: ?? x(); //output: ?? x(); //output: ?? y();//output: ??


1.مقدار x رو برابر با NumberFunc میزاریم و این فانکشن برای اولین بار اجرا میشه ، متغیر i ساخته میشه با مقدار 0 بعدش فانکشن incrementNumberFunc توی x ذخیره میشه با اینکه اینجا کار فانکشن NumberFunc تمومه و متغیر i هم نابود میشه اما مقدار i توی closure وجود داره و وقتی x اولین بار اجرا میشه فانکشن incrementNumberFunc اجرا میشه متغیر b ساخته میشه با مقدار 10 ، حالا مقدار 0 رو از i ای که داره نشون میده و 10 رو هم از b میگیره و نشون میده و حالا که کار فانکشن تموم شد دیگه متغیر b نابود میشه این پروسه توی متغیرy هم انجام شده و بعد از اجرا همین نتیجه رو برمیگردونه

2. بار دوم که x صدا زده میشه همین پروسه دوباره تکرار میشه اما ایندفعه متغیر i رو که از کلاژر گرفتیم و از پروسه قبلی مقدارش ++ شده و مقدار 1 رو نشون میده ولی دیگه b ذخیره نشده و از نو ساخته میشه و همون 10 رو نشون میده

3. بار سوم که x اجرا میشه بازم همین پروسه اجرا میشه و مقدار 2 برای i و 10برای b رو نشون میده

4. حالا y که اجرا بشه چون موقع تعریف کردنش i مساوی با 0 ذخیره شده پس این اولین باره اجرای y هستش و مثل اولین اجرای x هستش و مقدار 0 رو برای i و 10 برای b نشون میده پس در نتیجه اینطوری میشه:

x(); //output : i = 0 , b = 10 x(); //output : i = 1 , b = 10 x(); //output : i = 2 , b = 10 y(); //output : i = 0 , b = 10

پس closure ترکیبی از فانکشن و قابلیت به یاد سپردن متغیر های درون محدوده خارجیش هستش.


3. بحث Hoisting **

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

البته باید اینو به بگم که جاوااسکریپت فقط اسم متغیر رو با مقدار پیش فرض undefined میبره بالا میزاره و مقداری که ما به متغیر دادیم سر جای خودش میمونه

توی جاوااسکریپت وقتی میخوایم یه متغیر تعریف کنیم سه تا مرحله در پیش رو داره:

1. اعلام کردن یا declaration مثل:

let a ;

2. مقداردهی کردن یا initialisation /assignment مثل:

a = 'sam'

3. استفاده کردن یا usage مثل:

console.log(a + 'is developer');

ما معمولا این مراحل رو توی یه خط پیش میبریم به این صورت :

let a = 'sam'

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

یه نکته دیگه ای که باید به یاد داشته باشین اینه که اگه یه مقداری رو به متغیری بدین که اصلا declare یا اعلام نشده جاوااسکریپت خودش میاد توی محدوده global یه متغیر با اون اسم اعلام میکنه :) مثال:

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

حالا بیاید جلو وپشت پرده hoisting رو چند تا مثال ببینیم:

در محدوده جهانی یا global scope

چیزی که ما میبینیم:

console.log(hoistedVar); //output : undefined var hoistedVar = 'some random value'

چیزی که جاوااسکریپت اجرا میکنه:

var hoistedVar; console.log(hoistedVar) //output: undefined hoistedVar = 'some random value'

در محدوده محلی یا local scope

چیزی که ما میبینیم:

function hoist() { console.log(message); var message='Hoisting is all the rage!' } hoist();// output: undefined

چیزی که جاوااسکریپت اجرا میکنه:

function hoist() { var message; console.log(message); message='Hoisting is all the rage!' } hoist();// output: undefined

فک نمیکنم مثالای بالا نیاز به توضیح بیشتر داشته باشه پس با توجه به این مثالا پیشنهاد میشه که همیشه قبل از اجرا متغیر رو تعریف و مقداردهی کنیم

از وقتی let و const در es6 معرفی شدند یکی دیگه از تفاوت های این دو با var این هست که اینجور مواقع مچ جاوااسریکپترو میگیره بجای اینکه مقدار آندیفایند رو بهت برگردونه رسما بهت ارور میده که آقا یا خانم تو هنوز اینو مقداردهی نکردی پس حق نداری ازش استفاده کنی مثال:

console.log(hoistedVar); //Output: ReferenceError: hoist is not defined ... let hoistedVar = 'some random value'

البته اینو باید بدونید که اگه دستی بیاید با let یا const یع متغیر تعریف کنید ولی بهش مقدار ندید و قبل مقدار دادن استفاده کنید دیگه ارور رو نمیده و همون آندیفایند رو میده مثال:

let hoistedVar; console.log(hoistedVar); // Output: undefined hoistedVar= 'some random value'

حالا برای فانکشن و کلاس هم چندتا مثال بزنیم تا تکمیل شه این بحث...

فانکشن ها

دو نوع فانکشن میتونیم توی جاوااسکریپت بنویسیم:

1. نوع اول : Function declarations که به اینصورت نوشته میشه:

function someFunc(){ //code }

2. نوع دوم : Function expressions که به اینصورت نوشته میشه:

var someFunc = function(){ //code }


نوع اول Function declarations :

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

someFunc(); // output: 'hi' function someFunc(){ console.log('hi') }

نوع دوم Function expressions :

توی این نوع فانکشن جاوااسکریپت فقط اسم رو میبره بالا و طبق روال ثابت بهش مقدار undefined میده و وقتی بخوایم استفاده کنیم بهمون ارور "TypeError: expression is not a function"میده چون داریم آندیفایند رو به عنوان فانکشن صدا میزنیم مثال:

someFunc(); // output: TypeError: expression is not a function var someFunc = function(){ console.log('hi') }

کلاس ها

مثل فانکشن ها ، دو نوع کلاس میتونیم توی جاوااسکریپت بنویسیم:

  1. 1. نوع اول : Class declarations که به اینصورت نوشته میشه:
class SomeClass { constructor(name, lasname) { this.name= name; this.lastname= lastname; } }

2. نوع دوم : class expressions که به اینصورت نوشته میشه:

var someClass = class { constructor(name, lasname) { this.name= name; this.lastname= lastname; } }


نوع اول class declarations :

توی این نوع کلاس جاوااسکریپت hoist نمیکنه و اگه قبل مقداردهی از کلاس استفاده کنیم ارور میده:

var user = new SomeClass(); user.name= 'sam'; user.lastname= 'aghapour'; console.log(user); // Output: ReferenceError: SomeClass is not defined class SomeClass { constructor(name, lasname) { this.name= name; this.lastname= lastname; } }


نوع دوم class expressions :

توی این نوع کلاس دقیقا مثل این نوعِ فانکشن، جاوااسکریپت فقط اسم رو میبره بالا و طبق روال ثابت بهش مقدار undefined میده و موقع استفاده ارور میده. مثال:

var user = new SomeClass(); user.name= 'sam'; user.lastname= 'aghapour'; console.log(user); // Output: TypeError: SomeClass is not a constructor var SomeClass = class { constructor(name, lasname) { this.name= name; this.lastname= lastname; } };

خیله خب این مقاله اینجا به پایان میرسه ، خسته نباشی و امیدوارم که یه چیزی یادگرفته و یه قدم دیگه تو اقیانوس بیکران جاوااسکریپت عمیق تر شده باشی :)


خدانگهدار و موفق باشی ?


جاوااسکریپتClosurehoistingscopejavascript
درباره ی تکنولوژی های حوزه ی فرانت اند مینویسم.
شاید از این پست‌ها خوشتان بیاید