در جاوااسکریپت، متغیرها میتوانند دو نوع داده را در خود نگه دارند که عبارتند از: مقادیر primitive و مقادیر reference. درک تفاوت بین این دو نوع داده برای نوشتن کد کارآمد و بدون اشکال بسیار مهم است. در این مقاله قصد داریم تا تفاوت بین مقادیر primitive و reference در جاوااسکریپت را باهم بررسی کنیم.
مقادیر Primitive دادههایی هستند که مستقیماً در یک متغیر ذخیره میشوند. این مقادیر شامل اعداد، بولینها، رشتهها، null و undefined میباشند.
وقتی یک مقدار Primitive را به یک متغیر اختصاص میدهیم، یک کپی از آن مقدار ایجاد و در حافظه ذخیره میشود. هر تغییری که در متغیر ایجاد شود، بر مقدار اصلی تأثیر نمیگذارد. به عنوان مثال:
let x = 5; let y = x; y = 10; console.log(x); // Output: 5 console.log(y); // Output: 10
در مثال بالا، به متغیر y
یک کپی از مقدار متغیر x
تخصیص داده شده است. وقتی مقدار y
را تغییر میدهیم، روی مقدار x
تاثیری نمیگذارد. این به این دلیل است که x
و y
متغیرهای جداگانه با مکانهای حافظه جداگانه هستند.
مقادیر Reference آبجکتهایی هستند که در حافظه ذخیره میشوند و از طریق یک Reference قابل دسترسی میباشند. این مقادیر شامل آرایهها، آبجکتها و توابع هستند.
هنگامی که یک مقدار Reference را به یک متغیر اختصاص میدهیم، یک Reference به مقدار اصلی ایجاد شده و در حافظه ذخیره میشود. هر تغییری که در متغیر ایجاد شود بر مقدار اصلی تأثیر میگذارد. به عنوان مثال:
let array1 = [1, 2, 3]; let array2 = array1; array2.push(4); console.log(array1); // Output: [1, 2, 3, 4] console.log(array2); // Output: [1, 2, 3, 4]
در مثال بالا، به متغیر array2
یک Reference به آرایه اصلی array1
اختصاص داده شده است. وقتی مقداری را به array2
پوش میکنیم، روی array1
نیز تأثیر میگذارد زیرا هر دو متغیر به یک مکان از حافظه اشاره میکنند.
وقتی یک مقدار primitive را به عنوان آرگومان به یک تابع ارسال میکنیم، یک کپی از آن مقدار ایجاد میشود و به تابع ارسال میگردد. هر تغییری که در متغیر داخل تابع ایجاد شود، بر مقدار اصلی تأثیر نمیگذارد. به عنوان مثال:
function addOne(x) { x++; return x; } let number = 5; console.log(addOne(number)); // Output: 6 console.log(number); // Output: 5
در مثال بالا، تابع addOne
یک کپی از مقدار number
دریافت میکند. وقتی مقدار x
را در داخل تابع افزایش میدهیم، روی مقدار number
تأثیری نمیگذارد.
هنگامی که یک مقدار reference را به عنوان آرگومان به یک تابع ارسال میکنیم، یک reference به مقدار اصلی ارسال میشود. هر تغییری که در متغیر داخل تابع ایجاد شود بر مقدار اصلی تأثیر میگذارد.
به عنوان مثال:
function addToArray(array) { array.push(4); return array; } let myArray = [1, 2, 3]; console.log(addToArray(myArray)); // Output: [1, 2, 3, 4] console.log(myArray); // Output: [1, 2, 3, 4]
در مثال بالا، تابع addToArray
یک reference به آرایه اصلی myArray
دریافت میکند. وقتی در داخل تابع مقداری را به آرایه push کنیم، بر myArray
نیز تأثیر میگذارد زیرا هر دو متغیر به یک مکان از حافظه اشاره میکنند.
ایجاد یک کپی از یک آبجکت، آرایه یا تابع زمانی میتواند مفید باشد که میخواهیم دادهها را بدون تأثیرگذاری بر روی دادههای اصلی تغییر دهیم. راههای مختلفی برای ایجاد یک کپی از یک آبجکت در جاوااسکریپت وجود دارد.
یکی از راههای ایجاد یک کپی سطحی از یک آبجکت، استفاده از سینتکس spread آبجکت است که در ECMAScript 2018 معرفی شد. سینتکس آن بسیار ساده بوده و به شکل زیر میباشد:
const originalObj = { name: "John", age: 30 }; const copyObj = { ...originalObj };
در این مثال، copyObj
یک آبجکت جدید با ویژگیهای مشابه originalObj
است. با این حال، تغییر copyObj
بر originalObj
تأثیری نخواهد گذاشت.
در مورد آرایهها، میتوانیم از متد slice()
برای ایجاد یک کپی سطحی از یک آرایه استفاده کنیم. در ادامه یک مثال برای بررسی این موضوع داریم:
const originalArr = [1, 2, 3, 4]; const copyArr = originalArr.slice();
copyArr
یک آرایه جدید با همان مقادیر originalArr
است.
زمانی که میخواهیم یک کپی از یک تابع ایجاد کنیم موضوع ممکن است کمی پیچیدهتر باشد.یک رویکرد ایجاد یک تابع جدید است که به سادگی تابع اصلی را با همان آرگومانها فراخوانی میکند. به عنوان مثال:
function originalFunc(arg1, arg2) { // function body here } const copyFunc = function(...args) { return originalFunc.apply(this, args); };
در این مثال، copyFunc
یک تابع جدید است که originalFunc
را با همان آرگومانها فراخوانی میکند. اما باید این موضوع را به خاطر داشته باشیم که این کار فقط یک کپی سطحی از تابع ایجاد میکند. هر تابع یا آبجکت مورد استفاده در originalFunc
همچنان به مقادیر اصلی reference خواهند داشت.
همانطور که میبینیم، ایجاد کپی از آبجکتها، آرایهها و توابع میتواند یک تکنیک مفید در برنامه نویسی جاوااسکریپت باشد. استفاده از روش مناسب برای نوع دادهای که با آن کار میکنیم کمک میکند تا اطمینان حاصل کنیم که کد ما مطابق انتظاری که داریم عمل میکند و در طولانی مدت قابل نگهداری میباشد.
در زبان برنامه نویسی جاوااسکریپت دو راه برای کپی کردن آبجکتها و آرایه ها وجود دارد: کپی سطحی (shallow copy) و کپی عمیق (deep copy). درک تفاوت بین این دو نوع کپی مهم است، زیرا میتواند بر رفتار کد ما تأثیر بگذارد.
یک کپی سطحی یک آبجکت یا آرایه جدید ایجاد میکند، اما فقط referenceها به ویژگیهای آبجکت یا آرایه اصلی را کپی میکند.
به عبارت دیگر، آبجکت یا آرایه جدید دارای مقادیر مشابهی برای ویژگیهای خود است، اما خود ویژگیها همچنان به همان مقادیر در حافظه اشاره میکنند. این بدان معنی است که هر تغییری که در ویژگیهای آبجکت یا آرایه جدید ایجاد شود، آبجکت یا آرایه اصلی را نیز تحت تأثیر قرار میدهد و بالعکس.
در ادامه یک مثال برای بررسی کپی سطحی از یک آرایه را بررسی میکنیم:
const originalArr = [1, 2, 3, [4, 5]]; const shallowCopyArr = [...originalArr]; shallowCopyArr[0] = 10; shallowCopyArr[3][0] = 40; console.log(originalArr); // [1, 2, 3, [40, 5]] console.log(shallowCopyArr); // [10, 2, 3, [40, 5]]
در این مثال، عملگر spread برای ایجاد یک کپی سطحی از originalArr
استفاده میشود. سپس، اولین المنت shallowCopyArr
به ۱۰
تغییر میکند که بر روی originalArr
تأثیری نمیگذارد. اما وقتی اولین المنت آرایه تو در تو در shallowCopyArr
به ۴۰
تغییر میکند، در آرایه originalArr
نیز تغییر اتفاق میافتد، زیرا هر دو آرایه به یک آرایه تو در تو reference دارند.
از طرف دیگر، کپی عمیق یک آبجکت یا آرایه جدید ایجاد میکند. همچنین مقادیر ویژگیهای آبجکت یا آرایه اصلی را به جای referenceها کپی میکند. این بدان معنی است که هر تغییری که در آبجکت یا آرایه جدید ایجاد شود، آبجکت یا آرایه اصلی را تحت تأثیر قرار نمیدهد و بالعکس.
در در ادامه یک نمونه از کپی عمیق از یک آرایه را بررسی میکنیم:
const originalArr = [1, 2, 3, [4, 5]]; const deepCopyArr = JSON.parse(JSON.stringify(originalArr)); deepCopyArr[0] = 10; deepCopyArr[3][0] = 40; console.log(originalArr); // [1, 2, 3, [4, 5]] console.log(deepCopyArr); // [10, 2, 3, [40, 5]]
در این مثال برای تبدیل originalArr
به رشته از JSON.stringify()
استفاده میکنیم، سپس برای تبدیل مجدد رشته به آرایه JSON.parse()
را به کار میگیریم. این کار یک آرایه جدید با مقادیر مشابه originalArr
ایجاد میکند، اما آرایه جدید کاملاً مستقل از آرایه اصلی است.
در نتیجه، تفاوت اصلی بین یک کپی سطحی و کپی عمیق در جاوااسکریپت این است که آیا آبجکت یا آرایه جدید فقط referenceها به ویژگیهای آبجکت یا آرایه اصلی را کپی میکند یا اینکه مقادیر این ویژگیها را نیز شامل میشود.
به طور خلاصه، درک تفاوت بین مقادیر primitive و reference در جاوااسکریپت برای نوشتن کد کارآمد و بدون اشکال ضروری است. با آگاهی از نحوه ذخیره و دستکاری دادهها، میتوانیم از رفتارهای غیرمنتظره جلوگیری کنیم و عملکرد برنامههای خود را بهبود بخشیم.
نکتهای که باید به یاد داشته باشیم این است که در جاوااسکریپت مقادیر تایپهای primitive همراه با value ارسال میشوند، در حالی که آبجکتها و آرایهها با reference ارسال میگردند. همچنین این موضوع را باید هنگام کار با توابع و تخصیص متغیرها نیز در نظر داشته باشیم.
شایان ذکر است که ECMAScript 6 یک کلمه کلیدی جدید به نام let معرفی کرد که بیشتر شبیه زبانهای برنامه نویسی سنتی در رابطه با انتساب متغیر عمل میکند. let به ما این امکان را میدهد که یک متغیر با محدوده بلاک تعریف کنیم، به این معنی که متغیر فقط در داخل بلاک کدی که در آن تعریف شده است قابل دسترسی میباشد. این موضوع میتواند با محدود کردن دامنه متغیر، از ایجاد سردرگمی جلوگیری نماید.
در پایان، در حالی که مقادیر primitive و reference ممکن است جزییات کوچکی در طرح بزرگ برنامه نویسی جاوااسکریپت به نظر برسند، اما میتوانند تأثیر قابل توجهی بر رفتار و کارایی کد ما داشته باشند. با درک تفاوتها و استفاده از تکنیکهای مناسب برای موقعیت خاص خود، میتوانیم کدهای تمیزتر، مؤثرتر با خطای کمتر بنویسیم.