درباره این پست
توی این پست، اول یاد میگیریم که Hoisting توی جاوااسکریپ چیه و چه شکلی کار میکنه. همینطور یه سری نکات توی موارد خاص داره که اونا رو هم بررسی میکنیم.
فهرست مطالب
مفهوم Hoisting در JavaScript
طبق وب داکیومنت MDN،
"Hoisting در جاوااسکریپت به فرآیندی اشاره دارد که در آن مفسر ظاهراً تعریف توابع، متغیرها، کلاسها یا ایمپورتها را قبل از اجرای کد به بالای محدوده آنها منتقل میکند."
مرجع: MDN Hoisting
به عبارت سادهتر، قبل از اینکه کد اجرا بشه، هستهی جاوااسکریپت کد شما رو توی هر اسکوپ (یا محدوده) چک میکنه و اگه داخلش تعریفی انجام شده باشه اونو به ابتدای اون محدود منتقل میکنه. ابتدای اسکوپ یا محدوده کد میتونه خطوط اول ماژول یا همون فایل باشه یا مثلا خطوط اول داخلی یه فانکشن یا کلاس.
با توجه به تعریف بالا و قدرتی که Hoisting به جاوااسکریپت میبخشه، شما میتونید متغیر رو بعد از اینکه استفاده کردید یا بهش مقدار دادید تعریف کنید.
این رفتار جاوااسکریپت باعث میشه که ما بتونیم یک کد رو مثل مورد پایین بنویسیم:
1 a = 1; // مقداردهی به متغیر 2
3 console.log(a); // خروجی کنسول: 1 4
5 var a; // تعریف متغیر
کد بالا قبل از اجرا به حالت پایین تبدیل میشه (البته علیالظاهر):
1 var a; // تعریف متغیر به ابتدا منتقل میشه! 2 a = 1; // مقداردهی به متغیر 3
4 console.log(a); // خروجی کنسول: 1
به صورت کلی و طبق موارد بالا با مفهوم hoisting آشنا شدید. در ادامه عمیقتر بررسیش میکنیم!
تایپها
قبل از اینکه بخوایم مفهوم Hoisting رو توی تایپهای مختلف بررسی کنیم، مهمه که همطونطور که اشاره شد این رو در نظر داشته باشید که در جاوااسکریپت Hoisting فقط و فقط روی خطوطی که تعریف یا declaration داخلش اتفاق افتاده اعمال میشه، یعنی روی مقداردهیها یا initializations تاثیری نمیذاره و اعمال نمیشه. به عبارت دیگه، جایی که متغیر تعریف کردید Hoist میشه اما اگه یه جایی به یه متغیر مقدار داده باشید، اون مقداردهی همونجا باقی میمونه و توسط هسته جاوااسکریپت قبل از اجرا به بالا منتقل نمیشه.
برای اینکه این ماجرا رو بهتر متوجه بشید، این دو مثال رو ببینید:
1 var a = 1; 2 var b = 2; 3
4 console.log(`a=${a} and b=${b}`); // نتیجه لاگ: a=1 و b=2
1 var a = 1; 2
3 console.log(`a=${a} and b=${b}`); // نتیجه: a=1 و b=null 4
5 var b = 2;
مثال اول به درستی مقادیر رو توی کنسول لاگ میکنه، توی مثال دوم ما Hoisting رو داریم چون اگه متغیر b
به بالا منتقل نمیشد به جای مقدار null
باید undefined
لاگ میشد توی کنسول، اما چیزی که اتفاق افتاده اینکه مقداردهی به متغیر b
به بالا منتقل نشده. پس میشه نتیجه گرفت که منتغیر b
به بالا hoist شده اما مقدارش رو موقع بالا بردن null در نظر گرفته هسته جاوااسکریپ تا وقتی اجرای کد به خط پنجم که مقداردهی داخلش اتفاق میفته میرسه، مقدارش هم داخلش قرار بگیره.
همونطور که میدونید توی جاوااسکریپت هم تایپهای متفاوتی داریم و هم روشهای متفاوتی برای تعریف یه متغیر داریم. به همین دلیل الزامیه که اگه میخواید hoisting رو به خوبی درک کنید، حالات استثنا یا همون edge caseهاش رو هم در جریان باشد.
‑ Hoisting در متغیرها
توی JS، متغیرها رو میشه با کلماتکلیدی متفاوتی تعریف کرد. مثل var
یا let
و یا const
. هر سهتای این کلمات کلیدی hoist میشن به بالا و تفاوتشون توی شیوهای هست که مقداردهی اولیه میشن
— Hoisting در var
اگه متغیری تعریف بشه، جیاس اونو به ابتدای محدوده یا اسکوپش منتقل یا hoist میکنه. تا این جای این مقاله، تمام مثالا با استفاده از کلمهی کلیدی var
نوشته شدن. اگه نیاز دارید که مجدد ببینیدشون، مثال بخش مرتبط با مفهوم رو چک کنید.
به صورت کلی اگه از var
برای تعریف یه متغیر استفاده کنید، hoisting اتفاق میفته. همینطور اگه استفاده از متغیر قبل از مقدار دهی باشه، جاوااسکریپ مقدار اونو به صورت null
در نظر میگیره.
1 console.log(a); // نتیجه کنسول: null 2
3 a = 1; 4 var a;
— Hoisting در let
اگه متغیری با استفاده از let
تعریف بشه به بالای اسکوپ منتقل یا hoist میشه. تفاوت این کلمهکلیدی با کلمهی var
در این هست که اگه قبل از مقداردهی اولیه به متغیری که توسط let
تعریف شده بخواین از اون متغیر استفاده کنید، به خطا از جنس ReferenceError
بر میخورید. این خطا رو زمان اجرا یا در ران تایم دریافت میکنید.
1 console.log('hi'); 2
3 a = 1; 4 let a;
نتیجه اجرای کد بالا میشه این:
hi
ReferenceError: Cannot access 'a' before initialization
— Hoisting در const
مشابه دو مورد قبلی، متغیرهای تعریف شده توسط کلید const
هم hoist میشن. تفاوت برای این مورد در اینه که اگه قبل از مقداردهی اولیه بخواین از متغیری که با const
تعریف شده استفاده کنید، کد شما اصلا اجرا نخواهد شد و خطای سینتکسی رخ میده.
1 console.log('hi'); 2
3 a = 1; 4 const a;
نتیجه کد بالا به محض اینکه بخواین اجراش کنید میشه خروجی پایین، دقت کنید که برخلاف let
کد اجرا نمیشه:
SyntaxError: Missing initializer in const declaration
در رابطه با متغیرها و توی داکیومنتهای رسمی ممکنه با عبارت "Temporal Dead Zone" یا به فارسی "منطقه مرده زمانی" رو به رو بشید. طبق داکیومنتها وقتی از کلمات کلیدی let
یا const
قبل از مقداردهی اولیه استفاده کنید، اون متغیر در زمان قبل از مقداردهی در یک منطقه مرده قرار داره.
به عبارت سادهتر، "temporal dead zone" در واقع همون زمان بین ساخته شدن یک متغیر و مقداردهی اولیهاش هست. برخلاف کلمهکلیدی var
که همزمان با hoist یه متغیر، قرار گرفتنش توی مموری یا حافظه هم انجام میشه، برای کلمات کلیدی let
و const
این اتفاق نمیفته. برای مثال اگه یه متغیر دارید داخل کد که توی خط ۵ بهش مقدار دادید، وقتی مفسر یا interpreter جاوااسکریپت اون رو به بالا منتقل میکنه، صرفا مشخص میکنه برای خودش که همچین متغیری وجود داره ولی اینکه به اون متغیر مقدار بخواد بده دقیقا وقتی به خط ۵ برسه این کار رو انجام میده. پس این رو هم در نظر داشته باشید که memory allocation یا اختصاص حافظه به متغیر فقط برای کلمه کلیدی var
در زمان hoisting اتفاق میفته اون هم فقط مقداردهی اولیه که برابر با null
هست.
‑ Hoisting در توابع یا functionها
فانکشنها یا توابع جاوااسکریپت هم قابلیت hoist شدن رو دارن. این مورد در حقیقت میتونه از مزایای hoisting برای دوولوپرها هم باشه، چرا که میتونید فانکشنی که بعدتر تعریف شده رو زودتر فراخونی کنید بدون اینکه با مشکل مواجه بشید.
توی جاوااسکریپت به دو طریق میشه فانکشن یا تابع تعریف کرد، رفتار hoisting توی هر روش کمی متفاوت از اون یکیه:
— در Function Declaration
وقتی که توسط کلمه کلیدی function
تابع یا فانکشنی تعریف بشه شما میتونید قبل از تعریفش به اون فانکشن دسترسی داشته باشید. به عبار دیگه تمامی function declarationها به بالای کد hoist میشن.
برای مثال:
1 logOne(); // خروجی کنسول: 1 2
3 function logOne() { 4 console.log(1); 5 }
— در Function Expression
عبارات تابعی یا function expressionها هم به بالا hoist میشن با این تفاوت که نمیتونید قبل از مقداردهی بهش اونو فراخونی یا کال کنید. فراخونی یه function expression قبل از مقداردهی بهش باعث میشه خطا از جنس TypeError
بگیرید در زمان اجرای کد.
1 logOne(); // خروجی کنسول: ReferenceError: Cannot access 'logOne' before initialization 2
3 const logOne = function() { 4 console.log(1); 5 };
برای حالت عبارت تابعی یا function expression تفاوتی نمیکنه که تابع شما نامدار تعریف شده یا از arrow functionها استفاده کردید. در هر صورت مجاز به استفاده پیش از تعریف نیستید. بنابراین اگه نیاز دارین قبل از تعریف یه تابع یا فانکشن بتونید ازش استفاده کنید، از روش function declaration استفاده کنید.
‑ Hoisting در کلاسها
در جاوااسکریپت کلاسها هم hoist میشن، البته قبل از اینکه به یک کلاس مقداردهی کنید نمیتونید ازش استفاده کنید. چون کلاس توی "temporal dead zone" یا "منطقه مرده زمانی" قرار داره. استفاده از یک کلاس قبل از تعریفش باعث مواجهه با خطا از جنس ReferenceError
میشه.
1 const myObj = new MyNumberClass(); // خروجی کنسول: ReferenceError: Cannot access 'MyNumberClass' before initialization 2
3 class MyNumberClass { 4 constructor() { 5 this.value = 1; 6 } 7 }
چرا Hoisting در جاوااسکریپت وجود داره؟
جاوااسکرپیت قبل از اجرای کد اون رو parse یا تجزیه و تحلیل میکنه، با استفاده از hoisting مفسر یا interpreter این توانایی رو پیدا میکنه که از وجود متغیرها و توابع و کلاسها آگاه بشه و به توجه به این آگاهی:
- به استفاده شما از یه متغیر قبل از تعریفش گیر الکی نمیده، چون مطمئنه که اون متغیر در کد شما وجود داره
- باعث انعطافپذیری بیشتر کد و راحتی نوشتنش میشه چون ترتیب در تعریف توابع مهم نیست. (به نظر خودم این مورد باعث ارتقای DX یا Developer Experience یا تجربه دوولپر میشه)
منابع
مقالهای که مطالعه کردید در واقع تلاشی برای سادهتر بیان کردن محتویات دو رفرنس پایین بود و همینطور یکی کردنشون. اگه مایل بودید میتونید ببینیدشون.
جمعبندی
حتی با وجود اینکه توی جاوااسکریپت hoisting رفتار پیشفرض هست، ولی واقعیت اینه که میتونه یه جورایی عجیب به نظر برسه این مدل تفسیر کردن کدها. اگه یه توسعهدهنده یا developer در این رابطه ندونه یا به طور کامل راجع به رفتارش ندونه، میتونه منجر به ایجاد باگ یا رفتار عجیب داخل کد بشه. پس برای اینکه جلوی کلافگیو بگیرید و سری که در نمیکنه رو دستمال نبندید، همیشه تمام متغیرها رو توی بالاترین اسکوپ یا محدوده ممکن تعریف کنید.
امیدوارم این نوشته براتون مفید بوده باشه.