یکی از اساسیترین پارادایمهای تقریباً تمامی زبانهای برنامهنویسی، توانایی ذخیرهسازی مقادیر در متغیرها و سپس بازیابی یا تغییر آن مقادیر است. در واقع، توانایی ذخیرهسازی مقادیر و استخراج مقادیر از متغیرها است که به یک برنامه وضعیت میبخشد. بدون چنین مفهومی، یک برنامه میتواند برخی وظایف را انجام دهد، اما آن وظایف به شدت محدود و نه چندان جالب خواهند بود. اما اضافه شدن متغیرها به برنامه ما، جالبترین سوالاتی را که اکنون به آنها خواهیم پرداخت، ایجاد میکند: این متغیرها کجا زندگی میکنند؟ به عبارت دیگر، کجا ذخیره میشوند؟ و مهمتر از همه، چگونه برنامه ما آنها را وقتی که به آنها نیاز دارد پیدا میکند؟ این سوالات به نیاز به مجموعهای از قوانین خوب تعریفشده برای ذخیرهسازی متغیرها در مکانی خاص و برای یافتن آن متغیرها در زمان بعد اشاره دارند. ما آن مجموعه قوانین در جاوا اسکریپت را دامنه ( scope ) مینامیم. برای درک بهتر این مقاله بهتر است اول مقاله ی درک نحوه کارکرد کامپایلر جاوا اسکریپت را بخوانید.
آشنایی با دامنه ( scope ) در جاوا اسکریپت | lexical scope
دامنه ( scope ) در جاوا اسکریپت مجموعهای از قوانین برای جستجوی متغیرها با نام شناسایی آنها است. با این حال، معمولاً بیش از یک دامنه برای در نظر گرفتن وجود دارد. دو مدل غالب برای نحوه عملکرد دامنه وجود دارد. اولین مدل از این دو مدل به مراتب رایجترین است و توسط اکثر زبانهای برنامهنویسی استفاده میشود. این مدل دامنه لغوی (lexical scope) نامیده میشود، و ما آن را به طور عمیق بررسی خواهیم کرد. مدل دیگر، که هنوز توسط برخی زبانها استفاده میشود (مانند اسکریپتنویسی Bash، برخی حالتها در Perl) دامنه پویا (dynamic scope) نامیده میشود.
همانطور که مقاله ی کامپایلر در js گفته شد، اولین مرحلهی سنتی یک کامپایلر زبان استاندارد "توکنیزه کردن" نامیده میشود. اگر به خاطر داشته باشید، فرایند توکنیزه کردن یک رشته از کاراکترهای کد منبع را بررسی میکند و به توکنها معنای معنایی اختصاص میدهد که نتیجهی یک پارس حالتدار است. این مفهوم، مبنایی برای درک دامنهی لغوی (lexical scoop) و ریشهی نام آن فراهم میکند. برای تعریف آن به صورت دایرهای، دامنهی لغوی دامنهای است که در زمان توکنیزه کردن تعریف میشود. به عبارت دیگر، دامنهی لغوی بر اساس جایی که متغیرها و بلوکهای دامنه توسط شما در زمان نوشتن ایجاد میشوند، بنا شده است و بنابراین (عمدتاً) در زمان توکنیزه کردن کد شما، به صورت ثابت تعیین میشود.
این مفهوم، مبنایی برای درک دامنهی لغوی (lexical scope) و ریشهی نام آن فراهم میکند. برای تعریف آن به صورت دایرهای، دامنهی لغوی دامنهای است که در زمان توکنیزه کردن تعریف میشود. به عبارت دیگر، دامنهی لغوی بر اساس جایی که متغیرها و بلوکهای دامنه توسط شما در زمان نوشتن ایجاد میشوند، بنا شده است و بنابراین (عمدتاً) در زمان توکنیزه کردن کد شما، به صورت ثابت تعیین میشود.
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
}
foo( 2 ); // 2, 4, 12
سه دامنه تو در تو در این مثال کد وجود دارد. ممکن است مفید باشد که این دامنهها را مانند حبابهایی در داخل یکدیگر تصور کنید.
- حباب ۱ شامل دامنه جهانی است و تنها یک شناسه در آن دارد: foo
- حباب ۲ شامل دامنه foo است که سه شناسه دارد: a barو b
- حباب ۳ شامل دامنه barاست و تنها یک شناسه دارد: c
حبابهای دامنه با جایی که بلوکهای دامنه نوشته میشوند و کدام یک در داخل دیگری تو در تو است، تعریف میشوند. اما فعلاً فرض کنیم که هر تابع یک حباب جدید از دامنه ایجاد میکند. حباب bar کاملاً در داخل حباب foo قرار دارد، زیرا (و فقط به این دلیل) که ما انتخاب کردیم تابع bar را در آنجا تعریف کنیم.
ساختار و مکان نسبی این حبابهای دامنه به طور کامل به موتور توضیح میدهد که کجاها را باید برای یافتن یک شناسه جستجو کند. در قطعه کد قبلی، موتور دستور (..)console.log را اجرا میکند و به دنبال سه متغیر a، b و c میگردد. ابتدا از حباب دامنه درونیتر، یعنی دامنه تابع (..)bar شروع میکند. آنجا a را پیدا نمیکند، بنابراین یک سطح بالا میرود، به نزدیکترین حباب دامنه بعدی، یعنی دامنه (..)foo میرود. a را آنجا پیدا میکند و از همان a استفاده میکند. همین کار را برای b انجام میدهد. اما c را در داخل (..)bar پیدا میکند. اگر c هم در داخل (..)bar و هم در داخل (..)foo وجود داشت، دستور (..)console.log ابتدا c در داخل (..)bar را پیدا میکرد و از آن استفاده میکرد، و هرگز به c در داخل (..)foo نمیرسید. جستجوی دامنه هنگامی که اولین مطابقت را پیدا کند متوقف میشود. یک نام شناسه مشابه میتواند در چندین لایه دامنه تو در تو مشخص شود، که به این امر "سایهزنی" (shadowing) گفته میشود (شناسه داخلی "شناسه خارجی" را سایه میاندازد). صرفنظر از سایهزنی، جستجوی دامنه همیشه از دامنه داخلی در حال اجرا در آن زمان شروع میشود و به سمت بیرون بالا حرکت میکند تا اولین مطابقت را پیدا کند و سپس متوقف میشود.
تقلب در دامنه لغوی
اگر دامنه لغوی تنها با جایی که یک تابع اعلام میشود، تعریف شود که این تصمیم کاملاً در زمان نویسندگی است، چگونه ممکن است راهی برای "تغییر" (یا به اصطلاح، تقلب) در دامنه لغوی در زمان اجرا وجود داشته باشد؟ جاوااسکریپت دو مکانیزم برای این کار دارد. هر دوی این مکانیزمها در جامعهی گستردهتر به عنوان روشهای بد برای استفاده در کد شما مورد نکوهش قرار میگیرند. اما استدلالهای معمول علیه آنها اغلب نکتهی مهمترین را از دست میدهند: تقلب در دامنه لغوی منجر به عملکرد ضعیفتر میشود. قبل از اینکه مشکل عملکرد را توضیح دهم، بیایید ببینیم این دو مکانیزم چگونه کار میکنند.
eval
تابع (..)eval در جاوااسکریپت یک رشته را به عنوان آرگومان میگیرد و محتوای رشته را به عنوان کدی که در آن نقطه از برنامه نوشته شده بود، رفتار میکند. به عبارت دیگر، شما میتوانید به صورت برنامهریزیشده کد در داخل کد نوشته شدهی خود ایجاد کنید و کد تولید شده را به گونهای اجرا کنید که انگار در زمان نویسندگی وجود داشته است. با توجه به این دیدگاه، باید روشن شود که چگونه (..)eval به شما اجازه میدهد تا محیط دامنه لغوی را با تقلب تغییر دهید و وانمود کنید که کد زمان نویسندگی (یعنی، لغوی) همیشه در آنجا وجود داشته است. در خطوط بعدی کد پس از اجرای (..)eval، موتور نخواهد "دانست" یا "اهمیت نخواهد داد" که کد قبلی به صورت پویا تفسیر شده و بنابراین محیط دامنه لغوی را تغییر داده است. موتور به سادگی جستجوهای دامنه لغوی خود را همانطور که همیشه انجام میدهد انجام خواهد داد.
function foo(str, a) {
eval( str ); // تقلب!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1، 3
رشتهی var a = 3 در زمان اجرای (..)eval به عنوان کدی که همیشه آنجا بوده، تلقی میشود. چون این کد یک متغیر جدید به نام b اعلام میکند، دامنهی لغوی موجود تابع (..)foo را تغییر میدهد. در واقع، همانطور که قبلاً ذکر شد، این کد متغیر bرا در داخل تابع (..)foo ایجاد میکند که b اعلام شده در دامنه خارجی (جهانی) را سایه میاندازد. وقتی فراخوانی (..)console.log اتفاق میافتد، هم a و هم b را در دامنهی (..)foo پیدا میکند و هیچگاه به b خارجی نمیرسد. بنابراین، به جای اینکه ۱، ۲ چاپ شود، ۱، ۳ چاپ میشود، همانطور که به طور معمول اتفاق میافتاد.
With
تابع with دیگر ویژگی که در جاوااسکریپت برای تقلب در دامنه لغوی استفاده میشود و اکنون به شدت منع شده است، کلمه کلیدی with است. روشهای مختلفی برای توضیح with وجود دارد، اما من انتخاب میکنم آن را از دیدگاه نحوه تعامل و تاثیر آن بر دامنه لغوی توضیح دهم. with معمولاً به عنوان یک میانبر برای انجام مراجع چندگانه به ویژگیهای یک شیء بدون تکرار مرجع شیء توضیح داده میشود. برای مثال:
var obj = {
a: 1,
b: 2,
c: 3
};
obj.a = 2;
obj.b = 3;
obj.c = 4
// میانبر آسانتر
with (obj) {
a = 3;
b = 4;
c = 5;
}
با این حال، چیزهای بیشتری از این صرفاً یک میانبر برای دسترسی به ویژگیهای شیء در اینجا اتفاق میافتد. به مثال زیر توجه کنید:
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o3.a ); // undefined
console.log( a ); // 2 Oops, leaked global!
در این مثال کد، دو شیء o1 و o2 ایجاد میشوند. یکی دارای ویژگی a است و دیگری ندارد. تابع (..)foo یک مرجع شیء obj را به عنوان آرگومان میگیرد و { .. }with (obj) را بر روی مرجع فراخوانی میکند. در داخل بلوک with، ما یک مرجع معمولی به یک متغیر a، یک مرجع LHS در واقع، ایجاد میکنیم تا به آن مقدار ۲ اختصاص دهیم. وقتی o1 را میفرستیم، انتساب a = 2 ویژگی o1.a را پیدا میکند و آن را به مقدار 2 اختصاص میدهد، همانطور که در دستور console.log(o1.a) منعکس شده است. با این حال، وقتی o2را میفرستیم، چون آن دارای ویژگی a نیست، هیچ ویژگیای ایجاد نمیشود و o2.a همچنان undefined باقی میماند. اما سپس ما یک اثر جانبی عجیبی را مشاهده میکنیم، این که یک متغیر جهانی a با انتساب a = 2 ایجاد شده است. چگونه ممکن است؟ دستور with یک شیء را میگیرد، که میتواند دارای صفر یا بیشتر ویژگی باشد، و آن شیء را به گونهای رفتار میکند که گویی یک دامنه لغوی کاملاً جداگانه است و بنابراین ویژگیهای شیء به عنوان شناسههای لغوی تعریف شده در آن دامنه تلقی میشوند.
حال که با دامنه ( scope ) در جاوا اسکریپت آشنا شدید. می تواند از دیگر مقالات سایت Evolearn | ایوولرن نیز دیدن کنید.