حوزهی شناسهها (Identifiers Scope)
در بخشهای قبلی این فصل با انواع روشهای تعریف و فراخوانی توابع آشنا شدیم. همچنین در فصل دوم و سوم با روشهای مختلف تعریف و به کارگیری انواع دادههای اولیه، آرایهها، مجموعهها و ... نیز آشنا شدیم. ویژگی مشترک بین تمام این موارد، استفاده از شناسهها میباشد. یعنی هر متغیر، تابع، آرایه، مجموعه و ... دارای یک شناسه است.
در این بخش قصد داریم به این سوال پاسخ دهیم که این شناسهها در کدام نقاط برنامه در دسترس هستند؟ مثلاً آرایهای که در یک تابع تعریف میشود، آیا در توابع دیگر نیز قابل استفاده است یا خیر؟ در واقع هر شناسه در جاوا اسکریپت، با توجه به نحوهی تعریف و محل تعریف آن، در بخشهای مشخصی از برنامه در دسترس و قابل استفاده است. به محدودهای از برنامه که در آن یک شناسهی خاص در دسترس باشد، حوزهی (Scope) آن شناسه گفته میشود.
انواع مختلف حوزهها
در جاوا اسکریپت و سایر زبانهای برنامهنویسی انواع مختلفی از حوزههای دسترسی به شناسهها وجود دارد. در این کتاب ما با سه نوع زیر سر و کار خواهیم داشت :
- حوزهی سراسری (Global Scope) : شناسهای که حوزهی سراسری دارد، در تمام نقاط برنامه در دسترس (Accessible) یا قابل مشاهده (Visible) است.
- حوزهی تابع (Function Scope) : شناسهای که حوزهی تابع دارد، فقط در یک تابع در دسترس است و خارج از آن تابع قابل دسترسی نیست.
- حوزهی بلاک (Block Scope) : شاسهای که حوزهی بلاک دارد، فقط در یک بلاک کد در دسترس است و خارج از آن بلاک قابل دسترسی نیست.
توجه کنید که در هر سه مورد فوق، تمام شناسهها بعد از نقطهای که تعریف میشوند قابل استفاده هستند و قبل از آن قابل استفاده نیستند. مثلاً متغیری که در خط دهم یک تابع تعریف شده است را میتوان در خط بیستم همان تابع به کار برد، اما در خط پنجم تابع قابل استفاده نیست. البته در این رابطه جزئیات بیشتری وجود دارد که در بخش بعدی به آن خواهیم پرداخت.
حوزهی متغیرها
متغیرها در جاوا اسکریپت با توجه به نحوهی تعریف و محل تعریف، دارای حوزههای متفاوتی هستند. پیش از این در فصل دوم اشاره شد که یک متغیر را میتوان با یکی از کلمات کلیدی let یا const یا var تعریف کرد. که از const برای ثابتها و از let و var برای تعریف متغیرها استفاده میشود. توجه کنید که در اکثر مواقع به مجموعهی متغیرها و ثابتها به اختصار متغیر گفته میشود.
اما در فصل دوم به تفاوت بین let و var اشارهای نشد. تنها به ذکر این نکته اکتفا کردیم که let مزایایی نسبت به var دارد و به همین دلیل در تمام مثالها از let به جای var استفاده کردیم. در واقع تا قبل از انتشار استاندارد ES6، تنها راه تعریف متغیرها استفاده از کلمهی کلیدی var بود و کلمات کلیدی const و let در استاندارد ES6 به جاوا اسکریپت اضافه شدهاند. در ادامه قصد داریم به تفاوتهای بین این موارد اشاره کنیم.
کلمهی کلیدی var
در صورتی که متغیری در یک تابع با استفاده از کلمهی کلیدی var تعریف شود، دارای حوزهی تابع بوده و در تمام نقاط آن تابع در دسترس است. اما در خارج از آن تابع غیر قابل دسترسی خواهد بود. توجه کنید که متغیر مذکور میتواند از هر نوعی باشد. مثلاً میتواند یک عدد، رشته، آرایه یا مجموعه باشد. همچنین اگر متغیری خارج از محدودهی یک تابع با استفاده از کلمهی کلیدی var تعریف شود، دارای حوزهی سراسری بوده و در تمام نقاط برنامه در دسترس خواهد بود. به قطعه کد زیر توجه کنید.
var a = 10;
function example(){
var b = 20;
console.log(a);
console.log(b);
if(true){
console.log(a);
console.log(b);
}
}
example();
console.log(a);
console.log(b); // امکان دسترسی به این متغیر، خارج از تابع وجود ندارد. لذا خطا تولید خواهد شد
در مثال فوق دو متغیر با شناسههای a و b وجود دارد که متغیر a خارج از تابع و متغیر b در محدودهی یک تابع تعریف شده است. در نتیجه متغیر a دارای حوزهی سراسری بوده و در تمام برنامه قابل مشاهده است. اما متغیر b فقط در محدودهی تابعی که در آن تعریف شده است قابل مشاهده یا در دسترس خواهد بود. در صورتی که برنامهی فوق را اجرا کنید، با خروجی زیر در کنسول مواجه خواهید شد. این برنامه را میتوانید اینجا در CodePen اجرا کنید. البته در کنسول محیط CodePen خطای موجود در خط آخر را نخواهید دید و برای مشاهدهی این خطا حتماً باید خروجی را در کنسول مرورگر مشاهده کنید. (میتوانید کل برنامهی فوق را در کنسول مرورگر کپی کرده و اجرا کنید تا خطا را مشاهده کنید.)
← 10
← 20
← 10
← 20
← 10
← Uncaught ReferenceError: b is not defined
در برنامه فوق، ابتدا در خط ۱، متغیری به نام a و با مقدار اولیهی ۱۰ تعریف میشود. سپس در خطوط ۲ تا ۱۰ تابعی به نام example تعریف شده و در خط ۱۱ همین تابع فراخوانی میشود. یعنی با اجرای خط ۱۱، برنامه به خط ۳ رفته و اجرای کدهای داخل تابع example را شروع میکند.
در خط ۳ متغیری به نام b تعریف شده و مقدار اولیهی ۲۰ به آن داده میشود. سپس در خطوط ۴ و ۵ مقدار متغیرهای a و b چاپ میشود. توجه کنید که متغیر a دارای حوزهی سراسری است. بنابراین در هر جایی (حتی داخل توابع) قابل استفاده است. متغیر b نیز با توجه به این که در همین تابع تعریف شده است، میتوان داخل این تابع از آن استفاده کرد. به همین دلیل خطوط ۴ و ۵ بدون هیچ مشکلی اجرا شده و مقدار این دو متغیر را در دو خط اول خروجی چاپ میکنند.
در خط ۶ از ساختار شرطی if استفاده شده است. با توجه به اینکه مقدار شرط این ساختار برابر با true است. در نتیجه کدهای داخل آن باید اجرا شوند. باز هم با توجه به اینکه متغیر a دارای حوزهی سراسری است و متغیر b در همین تابع تعریف شده است. خطوط ۷ و ۸ نیز بدون مشکل اجرا میشوند و یک بار دیگر مقدار متغیر a و b را در کنسول نمایش میدهند.
پس از پایان اجرای تابع، برنامه به همان نقطهای که تابع فراخوانی شده بود (خط ۱۱) باز میگردد و اجرای برنامه را از دستور بعدی ادامه میدهد. در خط ۱۲، متغیر a باید در کنسول چاپ شود. با توجه به اینکه متغیر a حوزهی سراسری دارد، این دستور نیز بدون مشکل اجرا میشود و مقدار ۱۰ را در خروجی نمایش میدهد. اما در خط ۱۳ امکان دسترسی به متغیر b وجود ندارد. زیرا دیگر از محدودهی تابع example خارج شدهایم و متغیرهای تعریف شده در این تابع در این نقطه از برنامه قابل مشاهده نیستند. بنابر این دستور خط ۱۳ با خطا مواجه شده و اجرا نمیشود.
این مثال ساده به خوبی مفهوم حوزهی شناسهها را نشان میدهد. سعی کنید نحوهی عملکرد این مثال را با مطالعهی دقیق آن، به خوبی درک کنید. چرا که درک این مثال تاثیر زیادی در درک ادامهی این مبحث خواهد داشت.
کلمات کلیدی let و const
رفتار متغیرهایی که با کلمات کلیدی let و const تعریف میشوند، از نظر حوزهی دسترسی کاملاً یکسان است. این نوع متغیرها دارای حوزهی بلاک هستند. یعنی در هر بلاکی که تعریف شوند، فقط در همان بلاک و بلاکهای داخلی آن قابل مشاهده خواهند بود. در قطعه کد زیر چندین متغیر با کلمهی کلیدی let تعریف شدهاند و در نقاط مختلف قابل مشاهده بودن یا نبودن آنها نشان داده شده است.
let a = 10;
// a is visible
function example(){
let b = 20;
// a , b are visible
if(true){
let c = 30;
// a and b and c are visible
}
// a and b are visible
}
// a is visible
همانطور که مشاهده میکنید، متغیر a در تمام نقاط برنامه در دسترس است. زیرا در هیچ بلاکی قرار نگرفته و خارج از تمام توابع و بلاکها تعریف شده است. در این حالت هیچ تفاوتی بین استفاده از let و var برای تعریف متغیرها (از نظر حوزهی دسترسی) وجود ندارد. یعنی متغیری که خارج از تمام توابع و بلاکها تعریف شود، همیشه حوزهی سراسری دارد و در تمام نقاط برنامه در دسترس است.
اما متغیر b، با توجه به اینکه داخل یک تابع تعریف شده است. فقط در همین تابع و بلاکهای داخل آن در دسترس است. به همین دلیل در خط ۱۲ این متغیر در دسترس نیست. در این مورد نیز تفاوتی بین استفاده از let و var برای تعریف متغیر b وجود ندارد. چرا که متغیرهایی که با var تعریف میشوند نیز، فقط در همان تابعی که تعریف شدهاند در دسترس خواهند بود.
در واقع تفاوت اصلی رفتار let و var در متغیر c مشخص میشود. این متغیر در یک بلاک کد تعریف شده است. که در این مورد خاص این بلاک کد متعلق به یک ساختار شرطی if است. اما این موضوع اهمیتی ندارد. مهم این است که این متغیر در یک بلاک کد (یعنی داخل آکلاد باز و بسته) تعریف شده است. لذا فقط در همین بلاک، و بلاکهایی که در این بلاک تعریف شوند قابل مشاهده خواهد بود. به همین دلیل در خطوط ۱۰ و ۱۲ این متغیر قابل مشاهده نیست. اما در خط ۸ قابل مشاهده و در دسترس است. اگر برای تعریف متغیر c از کلمهی کلیدی var استفاده میشد، متغیر c در خط ۱۰ نیز قابل مشاهده بود. زیرا متغیرهایی که با var تعریف میشوند، در صورتی که داخل یک تابع تعریف شده باشند، در تمام نقاط آن تابع (مانند خط ۱۰) در دسترس خواهند بود. این در واقع تفاوت اصلی بین let و var است. همچنین توجه کنید که تمام نکات ذکر شده در مورد let، در مورد const نیز صادق است.
نکته : متغیرهایی که با کلمهی کلیدی let در قسمت initialization حلقههای for تعریف میشوند، فقط در بلاک کد مربوط به همان حلقه (و بلاکهای داخلی) قابل مشاهده هستند.
هرچند هنوز امکان استفاده از var برای تعریف متغیرها در جاوا اسکریپت وجود دارد. اما توصیه میشود تا زمانی که دلیل مشخصی برای استفاده از کلمهی کلیدی var ندارید، همیشه از let برای تعریف متغیرها استفاده کنید. در این کتاب نیز به همین ترتیب عمل خواهد شد.
تعریف متغیرها بدون استفاده از let - const - var
در جاوا اسکریپت این امکان وجود دارد که متغیرها را بدون کلمات var، let و const تعریف کنیم. به قطعه کد زیر دقت کنید.
a = 10;
console.log(a);
← 10
همانطور که مشاهده میکنید، متغیر a بدون این که از قبل با یکی از کلمات var یا let یا const تعریف شده باشد، برابر با ۱۰ قرار گرفته است. و در خط دوم مقدار این متغیر در کنسول چاپ میشود. این برنامه بدون هیچ مشکلی اجرا میشود و متغیر a نیز قابل استفاده خواهد بود. اما متغیرهایی که به این صورت تعریف میشوند یک تفاوت مهم با متغیرهایی که پیش از این با let، var یا const تعریف میکردیم دارند. چنین متغیرهایی همیشه دارای حوزهی سراسری هستند. یعنی حتی اگر داخل یک تابع یا یک بلاک تعریف شوند. در تمام نقاط برنامه در دسترس خواهند بود. به قطعه کد زیر توجه کنید.
function example(){
a = 10;
}
example();
console.log(a);
← 10
در مثال فوق در خط ۴ تابع example فراخوانی میشود. در نتیجه متغیری به نام a تعریف میشود. با توجه نحوهی تعریف این متغیر، میتوان در خارج از این تابع نیز به آن دسترسی داشت. لذا در خط ۵ امکان دسترسی به این متغیر وجود دارد و میتوان مقدار آن را چاپ کرد.
این روش برای تعریف متغیرها چندان مرسوم نیست و باید تا حد امکان از این روش پرهیز کنید. مگر در مواقعی که قصد دارید متغیری را در یک تابع تعریف کرده و در توابع دیگر به آن دسترسی داشته باشید. البته چنین مواردی در برنامهنویسی به ندرت بروز میکنند.
نکته : متغیرهایی که به روش فوق تعریف میشوند، در واقع به عنوان یک خاصیت به شئ window اضافه میشوند که در فصل نهم بیشتر در مورد آن بحث خواهد شد.
همپوشانی شناسهها
به طور کلی نمیتوان یک شناسه را در یک حوزه بیش از یک بار تعریف کرد. یعنی تعریف دو متغیر همنام در یک حوزه، حتی اگر از انواع متفاوتی باشند امکانپذیر نیست. اما میتوان در حوزههای مختلف، متغیرهایی با شناسهی یکسان تعریف کرد. به عنوان مثال اجرای قطعه کد زیر تولید خطا خواهد کرد، زیرا دو متغیر با نام یکسان در یک حوزه تعریف میشوند.
let a = 10;
const a = 20; //Uncaught SyntaxError: Identifier 'a' has already been declared
مشاهده میکنید که حتی با کلمات کلیدی مختلف نمیتوان دو متغیر با یک نام در حوزهی یکسان تعریف کرد. اما در قطعه کد زیر، دو متغیر با نام یکسان در دو حوزهی متفاوت تعریف میشوند که این کار در جاوا اسکریپت امکانپذیر است.
let a = 10;
function example(){
let a = 20;
console.log(a); // here a = 20
a = 30;
console.log(a); // here a = 30
}
console.log(a); // here a = 10
در چنین شرایطی، در هر نقطهای از برنامه که از متغیر a استفاده میکنیم. در واقع به متغیری که در نزدیکترین حوزه تعریف شده است دست خواهیم یافت. مثلاً در خط ۴ مقدار a برابر با ۲۰ است. زیرا در این نقطه منظور از متغیر a، متغیری است که در همین حوزه (حوزهی تابع) تعریف شده است. در خط ۵ نیز، وقتی مقدار متغیر a تغییر داده میشود. هیچ تاثیری بر متغیری که در خط ۱ تعریف شده است ندارد. یعنی مقدار متغیری که در خط ۳ تعریف شده است تغییر میکند و در خط ۶ نیز مقدار همین متغیر در کنسول چاپ میشود.
اما در خط ۸ از حوزهی تابع example خارج شدهایم. لذا دیگر امکان دسترسی به متغیری که در خط ۳ تعریف شده است وجود ندارد. در نتیجه در خط ۸ مقدار متغیر a که در خط ۱ تعریف شده است در کنسول چاپ خواهد شد.
توضیحات فوق را میتوان به این صورت خلاصه کرد :هرگاه از شناسهای در یک بلاک استفاده شود، اگر متغیری با آن شناسه در همان بلاک تعریف شده باشد، از همان متغیر استفاده خواهد شد. در غیر این صورت، یک بلاک بالاتر بررسی میشود و اگر چنین شناسهای در بلاک بالایی تعریف شده باشد، از آن استفاده خواهد شد. در غیر این صورت نیز به بلاک بالاتر مراجعه میشود و این روند تا رسیدن به بالاترین بلاک، یعنی حوزهی سراسری ادامه مییابد.
در کل بهتر است که تا حد امکان از متغیرهای همنام در یک برنامه استفاده نکنیم. اما اگر به هر دلیلی مجبور به انجام چنین کاری شدید، باید به نکات فوق توجه کنید تا در هر نقطهای از برنامه بتوانید به متغیری که واقعاً مورد نظرتان است دسترسی داشته باشید.