انواع مقدار و انواع ارجاع
در بخش قبلی دیدیم که انتساب یک متغیر از نوع Object به متغیری دیگر، باعث میشود تا هر دو متغیر به یک شئ واحد اشاره کنند. در نتیجه اعمال تغییرات به وسیلهی هر یک از متغیرها، شئ مورد نظر را تغییر داده و در واقع روی متغیر دیگر نیز تاثیر میگذارد. در این بخش قصد داریم به بررسی علت این رفتار بپردازیم که یکی از مهمترین مباحث در جاوا اسکریپت و به طور کلی در برنامهنویسی است.
ساختار حافظه و ذخیرهسازی متغیرها
هر سیستم کامپیوتری دارای بخشی به نام حافظهی اصلی یا حافظهی RAM است. تقریباً در تمام مثالهایی که پیش از این دیدیم، تعدادی متغیر برای ذخیرهسازی دادهها وجود داشت. حافظهی RAM در واقع همان محلی است که این دادهها در زمان اجرای برنامه، در آن ذخیره میشوند.
حافظهی RAM از تعدادی سلول حافظه (بایت) تشکیل شده است که هر سلول دارای یک شماره یا آدرس است. به طوری که سلول اول دارای آدرس صفر، سلول دوم دارای آدرس یک، سلول سوم دارای آدرس دو، ... و آخرین سلول از یک حافظهی n بایتی دارای آدرس n - 1 است. البته تنها برنامهای که امکان دسترسی به تمام این سلولها را بدون هیچ محدودیتی دارد، سیستم عامل است. سایر برنامهها که بعد از اجرای سیستم عامل قابل اجرا هستند، فقط میتوانند به بخشی از این حافظه دسترسی داشته باشند. این بخش از حافظه نیز توسط سیستم عامل تعیین میشود. مثلاً ممکن است برای برنامهای که نیاز به 1MB حافظه دارد، از آدرس 3M تا 4M در نظر گرفته شود. البته معمولاً برنامهها، از این موضوع اطلاع ندارند. یعنی از دید چنین برنامهای کل حافظه 1MB بوده که آدرس سلولهای آن از صفر تا 1M میباشد.
پس میتوان به اختصار اینگونه بیان کرد : هر برنامهی در زمان اجرا، بخشی از حافظهی RAM را در اختیار میگیرد که میتواند دادههای مورد نیاز خود را در آن بخش ذخیره کند. همچنین برنامهها به فضای حافظهی یکدیگر دسترسی ندارند. یعنی میتوان فرض کرد هر برنامه یک حافظهی RAM جداگانه برای خود دارد.
حال ببینیم که دادهها چگونه در این فضا ذخیره میشوند. حافظهای که در اختیار یک برنامه قرار میگیرد به دو بخش تقسیم میشود : حافظهی پشته یا Stack و حافظهی Heap. همانطور که پیشتر اشاره شد، دادهها در جاوا اسکریپت به دو دسته تقسیم میشوند. دادههای اولیه و اشیاء.
در جاوا اسکریپت تمام انواع دادهی اولیه در حافظهی Stack ذخیره میشوند و انواع مختلف اشیاء (Object، Array، Function و ...) در حافظهی Heap ذخیره میشوند. همچنین به دادههای اولیه و اشیاء به ترتیب انواع مقدار (Value Types) و انواع ارجاع (Reference Types) گفته میشود. دلیل این نامگذاری به نحوهی ذخیرهسازی این دادهها در حافظه مربوط میشود. این توضیحات را بر روی شکل بهتر میتوان درک کرد. پس بهتر است با یک مثال و بر روی شکل این مفاهیم را تشریح کنیم. قطعه کد زیر را در نظر بگیرید.
let a = 10;
let b = a;
با توجه به این که هر دو متغیر از انواع دادهی اولیه (Primitive) هستند. لذا هر دو باید در حافظهی Stack ذخیره شوند. شکل زیر وضعیت حافظهی Stack را بعد از اجرای خط اول و بعد از اجرای خط دوم نشان میدهد. (توجه کنید که فرض شده است که قبل از اجرای خط اول، حافظه کاملاً خالی بوده است.)
در این شکل فرض شده است که کل فضای حافظهی Stack از N سلول تشکیل شده و هر سلول نیز یک بایت در نظر گرفته شده است. البته در عمل میزان فضای لازم برای ذخیرهی هر متغیر بیش از یک بایت است. اما برای سادگی فعلاً هر سلول را یک بایت در نظر میگیریم.
میبینید که بعد از اجرای دستور اول، یک سلول از حافظهی Stack به متغیر a اختصاص یافته و مقدار ۱۰ در آن ذخیره میشود. همچنین پس از اجرای دستور دوم، یک سلول از حافظهی Stack به متغیر b اختصاص یافته و مقدار متغیر a در آن کپی میشود. در نتیجه مقدار سلول دوم نیز برابر با ۱۰ خواهد بود. از این به بعد این دو متغیر کاملاً از یکدیگر مستقل هستند و اعمال هر تغییری در یک متغیر، هیچ تاثیری در متغیر دیگر ندارد.
حال دستورات زیر را در نظر بگیرید.
let a = {
x: 10,
y: 20
};
let b = a;
دستور اول یک شئ را تعریف میکند که دارای دو خاصیت x و y است. این شئ را نمیتوان مانند حالت قبل در حافظهی Stack ذخیره کرد. زیرا در حافظهی Stack برای هر متغیر فقط یک سلول در نظر گرفته میشود و در یک سلول نمیتوان هم مقدار x و هم مقدار y را ذخیره کرد. حال تصور کنید که این شئ دارای صدها خاصیت و متد باشد. یا مثلاً یک آرایه با صدها عنصر باشد. مسلماً نمیتوان تمام این مقادیر را در یک سلول از حافظهی Stack ذخیره کرد. اما راه حل این مشکل چیست؟
برای حل این مشکل از حافظهی Heap و مفهومی به نام اشارهگر (Pointer) استفاده میشود. در این حالت شئ مورد نظر به طور کامل در حافظهی Heap ذخیره میشود و فقط آدرس محل ذخیرهسازی آن در حافظهی Stack در متغیر a ذخیره میشود. در نتیجه وضعیت حافظههای Stack و Heap پس از اجرای دستور اول به صورت زیر خواهد بود.
همانطور که مشاهده میکنید شئ مورد نظر (با نام فرضی Object1) در حافظهی Heap ذخیره شده است. لازم به ذکر است که ساختار حافظهی Heap نیز دقیقاً مانند حافظهی Stack است. یعنی از تعدادی سلول متوالی تشکیل شده است و در واقع مقدار هر یک از خاصیتهای x و y در یکی از این سلولها ذخیره میشود. اما برای سادگی، کل شئ Objcet1، به صورت یک موجودیت واحد نشان داده شده است. همچنین آدرس محل قرارگیری شئ Object1 در حافظهی Heap، در متغیر a ذخیره شده است. در این حالت اصطلاحاً گفته میشود که متغیر a به شئ Object1 اشاره میکند و متغیر a را یک اشارهگر مینامند. البته لفظ اشارهگر (Pointer) معمولاً در جاوا اسکریپت به کار برده نمیشود و بیشتر در زبانهایی مانند C و ++C از این اصطلاح استفاده میشود. ولی با این حال در این کتاب از این اصطلاح استفاده خواهیم کرد.
در مثال قبلی دیدیم که وقتی دستور "let b = a" اجرا میشد، مقدار متغیر a (یعنی ۱۰) در متغیر b کپی میشد. در این مثال نیز دقیقاً همین اتفاق میافتد. یعنی با اجرای خط دوم، مقدار متغیر a در متغیر b کپی میشود. اما نکتهی مهم در این حالت این است که مقدار متغیر a، یک آدرس از حافظهی Heap است. یعنی همین آدرس در متغیر b نیز کپی میشود. لذا پس از اجرای دستور دوم، وضعیت حافظههای Heap و Stack به صورت زیر خواهد بود.
میبینید که حالا متغیر b نیز به همان Object1 اشاره میکند. در نتیجه برای دسترسی و یا تغییر مقدار خاصیتهای Object1 از هر دو متغیر a و b استفاده کرد. یعنی دو دستور زیر در این حالت کاملاً معادل بوده و نتیجهی یکسانی خواهند داست.
a.x = 50;
// یا
b.x = 50;
از هر دو دستور فوق میتوان برای تغییر مقدار خاصیت x به ۵۰ استفاده کرد.
اما توجه به این نکته ضروری است که هنوز میتوان مقدار جدیدی را به هر یک از متغیرهای فوق نسبت داد. در این صورت دیگر این دو متغیر به یک شئ واحد اشاره نکرده و تغییر هر یک، مستقل از دیگری خواهد بود. مثلاً اگر دستور زیر را اجرا کنیم، متغیر a به یک دادهی اولیه از نوع عددی تبدیل میشود و دیگر امکان دسترسی به شئ Object1 از طریق این متغیر وجود ندارد.
a = 10;
پس از اجرای دستور فوق مقدار متغیر a برابر با ۱۰ خواهد شد و برای دسترسی به شئ Object1 فقط میتوان از متغیر b استفاده کرد.
نکته : متغیرهایی که با کلمهی کلیدی const تعریف میشوند (ثابتها)، بخش موجود در حافظهی Stack آنها ثابت و غیر قابل تغییر است. بنابراین اگر این متغیرها از نوع مقدار (یا انواع دادهی اولیه) باشند، به هیچ وجه قابل تغییر نیستند. اما اگر از نوع ارجاع باشند، شئ مرتبط با آنها که در حافظهی Heap ذخیره شده است قابل تغییر است. یعنی میتوان مقدار خاصیتها و متدهای اشیائی که با کلمهی کلیدی const تعریف شدهاند را تغییر داد. اما نمیتوان با عملگر انتساب "=" مقدار جدیدی را به متغیر نسبت داد.
ارسال اشیاء به توابع
انواع مختلف اشیاء را میتوان مانند دادههای اولیه به عنوان آرگومان به توابع و متدها ارسال کرد. اما اشیاء با توجه به این که از انواع ارجاع هستند، رفتار متفاوتی نسبت دادههای اولیه دارند. به قطعه کد زیر توجه کنید.
function example(a){
console.log('a = ' + a);
a = 40;
console.log('a = ' + a);
}
let b = 20;
example(b);
console.log('b = ' + b);
← "a = 20"
← "a = 40"
← "b = 20"
در مثال فوق متغیری به نام b با مقدار اولیهی ۲۰ در خط ۶ تعریف شده و به تابع example ارسال شده است. توجه کنید که ارسال متغیرها به توابع، فقط یک کپی از مقدار آنها را ارسال میکند. یعنی مقدار متغیر b در متغیر a کپی میشود. سپس در بدنهی تابع میتوان با استفاده از متغیر a از این مقدار استفاده کرد. همانطور که مشاهده میکنید، وقتی مقدار متغیر a برای اولین بار در خط ۲ چاپ میشود، برابر با ۲۰ است. اما در خط ۳ به ۴۰ تغییر میکند و در خط ۴ مجدداً در کنسول چاپ میشود. حال پس از خروج از تابع، دستور خط ۸ اجرا شده و مقدار متغیر b را چاپ میکند که هنوز برابر با ۲۰ است. یعنی تغییرات ایجاد شده در متغیر a در تابع example هیچ تاثیری در مقدار آرگومان ارسال شده (متغیر b) ندارد. زیرا فقط یک کپی از متغیر b به تابع ارسال میشود.
اما در صورت ارسال اشیاء (از هر نوعی) به توابع، تغییرات اعمال شده در خاصیتها و متدهای اشیاء در بدنهی تابع، بر آرگومان ارسال شده نیز تاثیر میگذارد. زیرا همانطور که اشاره شد، اشیاء از انواع ارجاع هستند. یعنی در زمان ارسال اشیاء، در واقع آدرس آن شئ در پارامتر ورودی تابع کپی میشود. لذا از طریق پارامتر ورودی تابع میتوان شئ ارسال شده را نیز تغییر داد. قطعه کد زیر این رفتار اشیاء را به خوبی نشان میدهد.
function example(a){
console.log('a.x = ' + a.x);
a.x = 40;
console.log('a.x = ' + a.x);
}
let b = {
x: 20
};
example(b);
console.log('b.x = ' + b.x);
← "a.x = 20"
← "a.x = 40"
← "b.x = 40"
میبینید که تغییر خاصیت x در خط ۳، موجب تغییر خاصیت x از شئ b نیز شده است. یعنی a.x و b.x هر دو برابر با ۴۰ هستند. این به دلیل کپی شدن مقدار متغیر b (یعنی آدرس شئ موجود در حافظهی Heap) در مقدار متغیر a است. در نتیجه از طریق متغیر a نیز میتوان به همان شئ دسترسی داشت و آن را تغییر داد. این مثال را میتوانید اینجا اجرا کنید.
در انتها ذکر این نکته ضروری است که مباحث مطرح شده در این بخش در مورد تمام انواع ارجاع صادق هستند. در مثالهای این بخش فقط از نوع Object استفاده شد. اما مثلاً آرایهها که نوعی شئ هستند و از انواع ارجاع به حساب میآیند نیز همین رفتار را دارند.