سلام به جی اس کارای عزیز ?
توی جاوا اسکریپت یه سری مباحث اولیه اما خیلی مهم هستن که یادگیریشون برای هر جی اس دولوپری واجبه که امروز قراره چندتا از اون مباحث شاملِ scopes , closures , hoisting رو زیر و رو کنیم و ببینیم چی هستن و چیکار میکنن و به چه دردی میخورن اما این زیر و رو کردن رو سعی میکنم به ساده ترین و کوتاه ترین شکل ممکن انجام بدم که نه گیج بشید نه حوصلتون سر بره..
اسکوپ یا محدوده ینی چی؟
اسکوپ، محدوده ای هستش که یه متغیر یا فانکشن یا ابجکت در دسترس و قابل استفاده هستش به همین سادگی
اسکوپ ها در جاوااسکریپت ؟
توی جاوااسکریپت دو نوع اسکوپ داریم:
متغیر هایی که داخل فانکشن ساخته میشن داخلِ local scope هستن و متغیر هایی که خارج از فانکشن ها ساخته میشن داخلِ global scope هستن
وقتی شروع به ساختن یه متغیر توی فایل جاوااسکریپت میکنی که داخل هیچ فانکشنی نیست شما این فانکشن رو داخل محدوده Global نوشتی و میتونی هر جایی حتی داخل local scope ها (داخل فانکشن ها) بهشون دسترسی داشته باشی :
var someThing = 'sam' console.log(someThing); //output: 'sam' function callSomeThing () { console.log(someThing); //output: 'sam' //متغیری که تو محدوده جهانی نوشتیم اینجا هم در دسترس هستش }
متغیر هایی که داخل فانکشن ها تعریف میکنیم داخل محدوده محلی یا local scope هستن و محدود به محدوده همون فانکشن هستن و نه هیچ جای دیگه(ینی نه داخل فانکشن های دیگه و نه داخل محدوده جهانی ، که البته یه استثنای کوچیکی هست که تو جلوتر بهش میرسیم)
//Global Scope function someFunc() { // Local scope 1 اولین محدوده محلی functoin someOtherFun(){ //local scope 2 دومین محدوده محلی } } function thirdFunc(){ //local scope 3 سومین محدوده محلی }
این وسط لازم به ذکره که محدوده های حلقه های 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
بالاتر گفتم که " متغیر هایی که داخل فانکشن ها تعریف میکنیم داخل local scope هستن و محدود به محدوده همون فانکشن هستن و نه هیچ جای دیگه که البته یه استثنای کوچیکی هست که تو جلوتر بهش میرسیم)"
خب اینجا همونجاییه که باید بگم موقع استفاده از متغیر داخل فانکشن یا local scope بحثی وجود داره به اسم lexical scope ( بعضیا static scope هم میگن بهش). حالا این ینی چی؟
این موضوع تو خیلی از زبونای برنامه نویسی وجود داره و اگه خیلی ساده بخوام بگم به این معنیه که اگه ما یه فانکشن داخل یه فانکشن دیگه داشته باشیم میتونیم به متغیرای محدوده ی محلیِ فانکشن پدرش دسترسی داشته باشه و به بیان دیگه هرچند تا فانکشن تو در تو داشته باشیم فانشکن فرزند علاوه بر متغیرای محدوده خودش به متغیرای محدوده پدرش و پدرِ پدرش و... دسترسی داره البته این رو در نظر بگیرید که این رابطه از بالا به پایینه و بر خلاف فانکشن فرزند ، فانکشن پدر نمیتونه به متغیرای محدوده فانکشن فرزند دسترسی داشته باشه! مثالش:
توی عکس بالا همونطور که از کامنتا مشخصه فانکشن بالاتر نمیتونه به متغیرای فانکشنای داخلش دسترسی داشته باشه اما فانکشن های فرزند میتونن به متغیرای فانکشنی که توش هستن دسترسی داشته باشن.
حالا که بحث static scope رو گفتیم بزارید یه نگاه کوچیکی هم داشته باشیم به dynamic scope تا کاملش کنیم. مثال زیر رو نگاه کنید تا فرق بین dynamic scope و lexical scope رو متوجه شیم.
توی بحث lexical scope اگه کد بالارو اجرا کنیم وقتی فانکشن b صدا زده میشه داخلش فانکشن a اجرا شده و فانکشن a داخلش someVar رو لاگ کرده خب وقتی توی a هیچ متغیری با اسم someVar نداریم بخاطر lexical بودن میاد اون محدوده ای که این فانکشن توش تعریف شده رو میگرده (پرنتش) تا ببینه میتونه این متغیر رو اونجا پیدا کنه و بله! متغیر someVar توی محدوده ای که فانکشن a تعریف شده وجود داره و برابر با 0 هستش پس خروجی نهایی 0 هستش.
حالا اگه بحث dynamic scope رو در نظر بگیریم توی کد بالا فانکشن a وقتی میبینه متغیر someVar وجود نداره بجای محدوده ی جایی که توش تعریف شده میره محدوده ای که توش "صدا زده شده" رو میگرده ینی محدوده محلیِ فانکشن b که متغیر someVar برابر با 1 هستش و خروجی نهایی 1 هستش.
نتیجه نهایی از مقایسه:
بحث scopes اینحا تموم میشه و حالا میریم سراغ closure
قابلیت closure توی جاوااسکریپت این امکان رو به فانکشن های داخل یا فانکشن دیگه میده که بتونه به متغیرای محدوده فانکشنی که توش تعریف شده دسترسی داشته باشه که تاالان با این قضیه تا حدی آشنا شدیم اما کاربرد و جالبیش هنوز مونده!
پس closure یه فانکشن داخلی هستش که به سه تا متغیرای سه نوع محدوده (scope-chain)دسترسی داره :
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 هم دسترسی داره
یه چیزی رو به خاطر داشته باشید که متغیرای داخل یه فانکشن تا زمانی وجود دارن که اون فانکشن داره اجرا میشه و به محض اینکه فانکشن اجرا و تموم بشه اون متغیر داخلش دیگه وجود نداره تا زمانی که دوباره فانکشن صدا زده بشه و اجرا شه حالا با توجه به این توضیحات بیاین این کد بالارو کامل قدم به قدم کالبد شکافی کنیم
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 ترکیبی از فانکشن و قابلیت به یاد سپردن متغیر های درون محدوده خارجیش هستش.
خب 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 رو چند تا مثال ببینیم:
چیزی که ما میبینیم:
console.log(hoistedVar); //output : undefined var hoistedVar = 'some random value'
چیزی که جاوااسکریپت اجرا میکنه:
var hoistedVar; console.log(hoistedVar) //output: undefined hoistedVar = 'some random value'
چیزی که ما میبینیم:
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') }
مثل فانکشن ها ، دو نوع کلاس میتونیم توی جاوااسکریپت بنویسیم:
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; } };
خیله خب این مقاله اینجا به پایان میرسه ، خسته نباشی و امیدوارم که یه چیزی یادگرفته و یه قدم دیگه تو اقیانوس بیکران جاوااسکریپت عمیق تر شده باشی :)
خدانگهدار و موفق باشی ?