hoisting در جاوااسکریپت به پروسه ایی گفته میشه که در آن مفسر کد جاوااسکریپت قبل از اجرای کد و بصورت ظاهری محل تعریف شدن یک فانکشن، متغیر یا کلاس رو به بالای scope منتقل میکنه و این رفتار پیشفرضی که جاوااسکریپت از خودش نشون میده باعث میشه تا ما بتونیم حتی قبل از محل نوشته شدن یک فانکشن از اون استفاده کنیم.
نکته: جاوااسکریپت بصورت ظاهری فقط محل تعریف شدن یک متغیر را به بالای scope منتقل میکنه، نه مقدار اون متغیر را.
همچنین جاوااسکریپت قبل از اجرا شدن کد، memory لازم را به فانکشن ها و متغیرها اختصاص میده.
ترتیب و چرخه متغیرها در جاوااسکریپت
در مواجه با متغیرها ما یک تعریف متغیر، مقداردهی متغیر و درنهایت استفاده از اون متغیر رو داریم، یعنی چیزی مثل کد زیر:
let a; // تعریف متغیر
a = 100; // مقدار دهی متغیر
console.log(a); // استفاده از متغیر
در مثال بالا صرفا برای نشون دادن چرخه و ترتیب متغیرها در جاوااسکریپت از تعریف و مقداردهی جداگانه استفاده کردیم و بجای کد بالا میتونیم همزمان متغیر را ایجاد و مقداردهی کنیم:
let a = 100;
البته این نکته را به خاطر داشته باشید که جاوااسکریپت در پشت صحنه و قبل از اجرا شدن کد همانند مثال اول ابتدا متغیر را تعریف و سپس مقدارد دهی میکنه.
یک حالت متفاوتی هم وجود داره، فرض کنید شما هیچوقت متغیری با نام a بوسیله var
یا let
یا const
تعریف نکردید، ولی در جایی از کد خودتون به شکل زیر a را مقدار دهی میکنید:
function hoist() {
a = 20;
var b = 100;
}
hoist();
console.log(a);
/*
خروجی : 20
be onvane yek variable global dar nazar gerefte mishe
*/
console.log(b);
/*
خروجی: ReferenceError: b is not defined
chon ba kalame var daroone function hoist tarif shode. scope oonham mahdood be function hast
*/
در کد بالا متغیر a به عنوان یک متغیر global در نظر گرفته میشه، بنابر این اینگونه متغیرهایی که قبلا تعریف نشدند به عنوان متغیرهای global شناخته میشن و این یکی از عجیب ترین کارهایی هست که جاوااسکریپت در مواجه با متغیرها انجام میده و از اینرو توصیه میشه تا همیشه متغیر مدنظر خودتون را در scope مدنظرتون تعریف کنید تا هم خوانایی کد بالاتر بره و هم از صحت و عملکرد کد خیالتون راحت باشه!
hoisting متغیرها در جاوااسکریپت
var hoisting
scope متغیرهای var
زمینه اجرای فعلی اونهاست. یعنی چی ؟ یعنی اگر متغیری با کلمه var
درون یک فانکشن ایجاد کنید، scope اون متغیر محدود به فانکشنی هست که در اون محصور شده و اگر خارج از هرگونه فانکشنی تعریفشون کنید، scope اونها global خواهد بود. بیایید چند مثال را باهم ببینیم:
متغیرهای global
console.log(hoist); // خروجی: undefined
var hoist = 'variable hoist shod';
ما پیش خودمون فکر میکردیم خروجی کد بالا مساوی با ارور ReferenceError: hoist is not defined
خواهد بود اما چنین نشد و بجاش با مقدار undefined روبرو شدیم! فکر میکنید دلیلش چیه ؟
دلیل اینکه در کد بالا با اروری مواجه نشدیم این هست که جاوااسکریپت محل تعریف شدن متغیر را ( variable declaration ) را قبل از اجرای هرگونه کدی به بالای scope منتقل کرد و اگر بخواهیم رفتار مفسر کد جاوااسکریپت در مواجه با کد بالا را شبیه سازی کنیم، میتونیم مثال زیر رو بزنیم:
var hoist;
console.log(hoist); // خروجی: undefined
hoist = 'variable hoist shod';
از اینرو ما میتونیم از متغیرها قبل از تعریف شدنشون هم استفاده کنیم، هرچند باید به خاطر داشت که مقدار متغیرهایی که قبل از تعریف شدنشون داریم ازشون استفاده میکنیم بصورت پیشفرض برابر با undefined
خواهد بود و توصیه ما این هست که همیشه قبل از استفاده از یک متغیر اونرو تعریف و مقدار دهی کنید تا کد تمیزتر و قابل اطمینان تری داشته باشید.
Function scoped variables
همانطور که بالاتر دیدیم، متغیرهای global-scope به بالاترین سطح hoist میشن. حالا بیایید باهم ببینیم که متغیرهای function-scope به چه صورت عمل میکنند و چگونه hoist میشن.
function hoist() {
console.log(message);
var message='hello world'
}
hoist();
طبق چیزهایی که تا اینجای مقاله باهم یادگرفتیم حدس بزنید که خروجی کد بالا چی خواهد بود ؟
اگر حدس شما مقدار undefined
هست که تبریک میگم! تا اینجای مقاله را خوب متوجه شدید و اگرهم حدستون اشتباه بوده که جای هیچ نگرانی نیست چراکه در ادامه مقاله به توضیح و برسی بیشتری میپردازیم.
کد زیر شکلی هست که مفسر جاوااسکریپت کد بالا رو میبینه:
function hoist() {
var message;
console.log(message);
message='hello world'
}
hoist(); // خروجی: undefined
scope متغیر message فانکشن hoist هست، بنابر این متغیر message به بالای scope فانکشن hoist منتقل میشه و برای جلوگیری از مواجه شدن با مقدار undefined بهتر هست که ابتدا متغیر را تعریف و مقدار دهی کنیم و سپس از اون استفاده کنیم، یعنی به شکل زیر:
function hoist() {
var message='hello world'
return (message);
}
hoist(); // خروجی: hello world
strict mode و var hoisting
به لطف ابزاری از نسخه es5 جاوااسکریپت که به عنوان strict mode شناخته میشه، میتونیم با حالت سختگیرانه تری متغیرهامون را تعریف کنیم. با فعال سازی حالت strict mode ما وارد یک حالت محدود تری از جاوااسکریپت میشیم که اجازه استفاده از یک متغیر را قبل از تعریف شدنش نمیده!
بطور کل استفاده از حالت strict mode تغییرات زیر را به همراه داره:
- در این حالت برخی از ارورهای اصطلاحا خاموش دیگه توسط جاوااسکریپت نادیده گرفته نمیشن.
- اشتباهاتی که بهینه سازی را برای موتورهای جاوااسکریپتی سخت میکنه را اصلاح میکنه.
- استتفاده از برخی syntax هایی که ممکنه در نسخه های آینده جاوااسکریپت اضافه بشه را منع میکنه.
حالت strict mode را میتونیم با نوشتن عبارت زیر در ابتدای فایل یا فانکشن مدنظرمون فعال کنیم:
"use strict";
حالا بیایید تستش کنیم:
'use strict';
console.log(hoist); // خروجی: ReferenceError: hoist is not defined
hoist = 'Hoisted';
در کدبالا اگر strict-mode فعال نبود متغیر ما به بالای scope خودش منتقل و hoist میشد، اما موقعی که strict mode فعال هست ما نمیتونیم از متغیری قبل از تعریف شدنش استفاده کنیم.
همچنین به خاطر داشته باشید که strict mode در مرورگرهای مختلف رفتار مختلفی داره و بهتره که همیشه از کدتون را تست و از درست رفتار کردنش اطمینان حاصل کنید.
let hoisting
قبل از اینکه hoisting متغیرهای let
در جاوااسکریپت را برسی کنیم این نکته را به خاطر داشته باشید که کلمه let
یک متغیر block-scope ایجاد میکنه و scope این متغیرها برخلاف var
مربوط به فانکشنی که در اون تعریف شدن نیست.
بیایید با برسی رفتار متغیرهای let
شروع کنیم:
console.log(hoist); // Output: ReferenceError: hoist is not defined ...
let hoist = 'test hoist variable';
مثال بالا را قبلا با کلمه var
نوشته بودیم و در اینجاهم انتظار داشتیم تا بجای ReferenceError
با مقدار undefined
مواجه بشیم. اما در ورژن es6 جاوااسکریپت، متغیرهای ایجاد شده با کلمه let برخلاف متغیرهای var
این امکان را به ما نمیدن که از متغیری قبل از تعریف شدنش استفاده کنیم و اصطلاحا hoist نمیشن و درصورت اینکار ما با ReferenceError
مواجه خواهیم شد.
از اینرو متغیرهای let
اطمینان حاصل میکنند تا ابتدا متغیر را تعریف و سپس از اون استفاده کنیم.
هرچند که میتونیم به شکل زیر ابتدا متغیر را تعریف و سپس مقدار دهی کنیم و در این صورت دیگه ReferenceError
نخواهیم گرفت.
let hoist;
console.log(hoist); // Output: undefined
hoist = 'test hoist'
لذا برای اینکه کد ما به ارور نخوره، نیازه تا ابتدا متغیر را تعریف و بعد مقداردهی کنیم و یا اینکه همزمان تعریف متغیر و مقدار دهی اونرو انجام بدیم.
const hoisting
متغیرهای const
در نسخه es6 اضافه شد و کلمه const
متغیری block-scope و اصطلاحا تغییر ناپذیر ایجاد میکنه. یعنی ما نمیتونیم بصورت مستقیم مقدار اینگونه متغیرهارو با استفاده از عملگر =
تغییر بدیم و صرفا زمان هایی که مقدار یک متغیر const
برابر با object یا array هست میتونیم مقادیر و محتوای اون object یا array را تغییر بدیم.
بیایید یک مثالی از تلاش برای تغییر مقدار یک متغیر const
بوسیله عملگر =
را ببینیم:
const PI = 3.142;
PI = 22/7;
console.log(PI); // خروجی: TypeError: Assignment to constant variable.
متغیرهای const
برخلاف var
و let
تغییر ناپذیر و اصطلاحا immutable variables هستند و ما نمیتونیم بصورت مستقیم و با استفاده از عملگر =
مقدار جدیدی را به اونها اضافه کنیم و درصورت اینکار با ارور TypeError: Assignment to constant variable
مواجه میشیم.
حالا بیایید یک مثالی هم از استفاده از یک متغیر const
قبل از تعریف شدنش ببینیم:
console.log(hoist); // خروجی: ReferenceError: hoist is not defined
const hoist = 'hoist test';
const
و let
در مثال بالا شبیه هم هستند و ما نمیتونیم قبل از تعریف شدن این متغیرها ازشون استفاده کنیم و درصورت اینکار با ارور ReferenceError: hoist is not defined
روبرو میشیم. در نتیجه فقط متغیرهای var
هستند که hoist میشن و به بالای scope منتقل میشن.
همچنین ارور بالا را در مثال زیر هم خواهیم دید و تفاوتی نداره که بخواهید از یک متغیر const
در بدنه فانکشن یا هرجای دیگری قبل از تعریف شدنش استفاده کنید:
function getCircumference(radius) {
console.log(circumference)
circumference = PI*radius*2;
const PI = 22/7;
}
getCircumference(2) // ReferenceError: circumference is not defined
تفاوت دیگر بین متغیرهای const
با let
و var
این هست که ما مجبوریم تا موقع تعریف کردن، اونهارو مقدار دهی هم بکنیم :
const PI;
console.log(PI); // خروجی: SyntaxError: Missing initializer in const declaration
hoisting functions
فانکشن ها در جاوااسکریپت را میتونیم بطور نسبی به 2 گروه تقسیم کنیم:
- Function declarations
- Function expressions
حالا بریم تا برسی کنیم که hoisting روی هرکدوم از گروه های بالا به چه صورت رفتار میکنه.
Function declarations
این گروه از فانکشن ها به شکل مثال زیر نوشته میشن و تا بالاترین سطح scope خودشون منتقل و hoist میشن. از اینرو ما میتونیم قبل از تعریف شدن این گروه از فانکشن ها ازشون استفاده کنیم.
hoisted(); // خروجی: "hoist test"
function hoisted() {
console.log('hoist test');
};
Function expressions
اما این گروه از فانکشن ها یعنی Function expressions برخلاف گروه قبلی hoist نمیشن.
expression(); //خروجی: "TypeError: expression is not a function
var expression = function() {
console.log('test hoist');
};
همچنین ترکیب این دو گروه در مثال زیر هم بی اثره و فقط در گروه و مثال اول فانکشن ما hoist و به بالای scope منتقل میشه:
expression(); // Ouput: TypeError: expression is not a function
var expression = function hoisting() {
console.log('hoist test');
};
قبلتر یادگرفتیم که متغیرهای var
به بالای scope منتقل میشن و ما میتونیم از اونها استفاده کنیم و نکته ایی که خیلی به اون اشاره کردیم این بود که فقط تعریف متغیر به بالای scope منتقل میشه و نه مقدار اون! بنابر این در مثال بالا متغیر expression
به بالا منتقل شده و مقدار اون undefined هست و زمانیکه ما میخواهیم متغیر expression
که مقدارش undefined هست را مثل یک فانکشن صدا بزنیم با ارور TypeError: expression is not a function
مواجه میشیم ( این ارور به ما میگه که این متغیر یک فانکشن نیست )
ترتیب ها و اولویت بندی ها
چند نکته حائز اهمیت درباره تعریف فانکشن ها و متغیرها در جاوااسکریپت وجود داره که باید به خاطر داشته باشید.
- انتساب مقدار به یک متغیر ( تعریف و مقداردهی همزمان یک متغیر ) بر تعریف یک فانکشن اولویت داره.
- تعریف یک فانکشن بر تعریف یک متغیر اولویت داره.
انتساب مقدار به یک متغیر بر تعریف یک فانکشن اولیت داره و تعریف یک فانکشن هم به تعریف یک متغیر اولویت داره ( این اولویت بندی ها از نظر انتقالشون به بالای scope هستش ).
بیایید با چند مثال این اولویت بندی هارو بیشتر برسی کنیم:
اولویت بیشتر انتساب مقدار به یک متغیر بر تعریف یک فانکشن
var double = 22;
function double(num) {
return (num*2);
}
console.log(typeof double); // Output: number
اولویت بیشتر تعریف فانکشن نسبت به تعریف متغیر
var double;
function double(num) {
return (num*2);
}
console.log(typeof double); // Output: function
در مثال بالا اگر جای تعریف متغیر و فانکشن را عوض کنیم هم خروجی ما فرقی نمیکنه و با مقدار function روبرو میشیم.