مقایسه مقادیر Primitive و Reference در جاوااسکریپت

در جاوااسکریپت، متغیرها می‌توانند دو نوع داده را در خود نگه دارند که عبارتند از: مقادیر primitive و مقادیر reference. درک تفاوت بین این دو نوع داده برای نوشتن کد کارآمد و بدون اشکال بسیار مهم است. در این مقاله قصد داریم تا تفاوت بین مقادیر primitive و  reference در جاوااسکریپت را باهم بررسی کنیم.

منظور از مقادیر Primitive چیست؟

مقادیر Primitive داده‌هایی هستند که مستقیماً در یک متغیر ذخیره می‌شوند. این مقادیر شامل اعداد، بولین‌ها، رشته‌ها، null و undefined می‌باشند.

وقتی یک مقدار Primitive را به یک متغیر اختصاص می‌دهیم، یک کپی از آن مقدار ایجاد و در حافظه ذخیره می‌شود. هر تغییری که در متغیر ایجاد شود، بر مقدار اصلی تأثیر نمی‌گذارد. به عنوان مثال:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
let x = 5;
let y = x;
y = 10;
console.log(x); // Output: 5
console.log(y); // Output: 10
let x = 5; let y = x; y = 10; console.log(x); // Output: 5 console.log(y); // Output: 10
let x = 5;
let y = x;
y = 10;
console.log(x); // Output: 5
console.log(y); // Output: 10

در مثال بالا، به متغیر

y
yیک کپی از مقدار متغیر
x
xتخصیص داده شده است. وقتی مقدار
y
y را تغییر می‌دهیم، روی مقدار
x
x تاثیری نمی‌گذارد. این به این دلیل است که
x
x و
y
y متغیرهای جداگانه با مکان‌های حافظه جداگانه هستند.

منظور از مقادیر Reference چیست؟

مقادیر Reference آبجکت‌هایی هستند که در حافظه ذخیره می‌شوند و از طریق یک Reference قابل دسترسی می‌باشند. این مقادیر شامل آرایه‌ها، آبجکت‌ها و توابع هستند.

هنگامی که یک مقدار Reference را به یک متغیر اختصاص می‌دهیم، یک Reference به مقدار اصلی ایجاد شده و در حافظه ذخیره می‌شود. هر تغییری که در متغیر ایجاد شود بر مقدار اصلی تأثیر می‌گذارد. به عنوان مثال:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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]
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]
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
array2یک Reference به آرایه اصلی
array1
array1اختصاص داده شده است. وقتی مقداری را به
array2
array2پوش می‌کنیم، روی
array1
array1نیز تأثیر می‌گذارد زیرا هر دو متغیر به یک مکان از حافظه اشاره می‌کنند.

چگونه مقادیر و Referenceها را به عنوان آرگومان تابع منتقل کنیم؟

وقتی یک مقدار primitive را به عنوان آرگومان به یک تابع ارسال می‌کنیم، یک کپی از آن مقدار ایجاد می‌شود و به تابع ارسال می‌گردد. هر تغییری که در متغیر داخل تابع ایجاد شود، بر مقدار اصلی تأثیر نمی‌گذارد. به عنوان مثال:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function addOne(x) {
x++;
return x;
}
let number = 5;
console.log(addOne(number)); // Output: 6
console.log(number); // Output: 5
function addOne(x) { x++; return x; } let number = 5; console.log(addOne(number)); // Output: 6 console.log(number); // Output: 5
function addOne(x) {
    x++;
    return x;
}

let number = 5;
console.log(addOne(number)); // Output: 6
console.log(number); // Output: 5

در مثال بالا، تابع

addOne
addOneیک کپی از مقدار
number
numberدریافت می‌کند. وقتی مقدار
x
xرا در داخل تابع افزایش می‌دهیم، روی مقدار
number
numberتأثیری نمی‌گذارد.

هنگامی که یک مقدار reference را به عنوان آرگومان به یک تابع ارسال می‌کنیم، یک reference به مقدار اصلی ارسال می‌شود. هر تغییری که در متغیر داخل تابع ایجاد شود بر مقدار اصلی تأثیر می‌گذارد.

به عنوان مثال:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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]
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]
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
addToArrayیک reference به آرایه اصلی
myArray
myArrayدریافت می‌کند. وقتی در داخل تابع مقداری را به آرایه push کنیم، بر
myArray
myArrayنیز تأثیر می‌گذارد زیرا هر دو متغیر به یک مکان از حافظه اشاره می‌کنند.

نحوه ایجاد یک کپی از آبجکت‌ها، آرایه‌ها و توابع

ایجاد یک کپی از یک آبجکت، آرایه یا تابع زمانی می‌تواند مفید باشد که می‌خواهیم داده‌ها را بدون تأثیرگذاری بر روی داده‌های اصلی تغییر دهیم. راه‌های مختلفی برای ایجاد یک کپی از یک آبجکت در جاوااسکریپت وجود دارد.

یکی از راه‌های ایجاد یک کپی سطحی از یک آبجکت، استفاده از سینتکس spread آبجکت است که در ECMAScript 2018 معرفی شد. سینتکس آن بسیار ساده بوده و به شکل زیر می‌باشد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const originalObj = { name: "John", age: 30 };
const copyObj = { ...originalObj };
const originalObj = { name: "John", age: 30 }; const copyObj = { ...originalObj };
const originalObj = { name: "John", age: 30 };
const copyObj = { ...originalObj };

در این مثال،

copyObj
copyObjیک آبجکت جدید با ویژگی‌های مشابه
originalObj
originalObjاست. با این حال، تغییر
copyObj
copyObjبر
originalObj
originalObjتأثیری نخواهد گذاشت.

در مورد آرایه‌ها، می‌توانیم از متد

slice()
slice()برای ایجاد یک کپی سطحی از یک آرایه استفاده کنیم. در ادامه یک مثال برای بررسی این موضوع داریم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const originalArr = [1, 2, 3, 4];
const copyArr = originalArr.slice();
const originalArr = [1, 2, 3, 4]; const copyArr = originalArr.slice();
const originalArr = [1, 2, 3, 4];
const copyArr = originalArr.slice();

copyArr
copyArrیک آرایه جدید با همان مقادیر
originalArr
originalArrاست.

زمانی که می‌خواهیم یک کپی از یک تابع ایجاد کنیم موضوع ممکن است کمی پیچیده‌تر باشد.یک رویکرد ایجاد یک تابع جدید است که به سادگی تابع اصلی را با همان آرگومان‌ها فراخوانی می‌کند. به عنوان مثال:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function originalFunc(arg1, arg2) {
// function body here
}
const copyFunc = function(...args) {
return originalFunc.apply(this, args);
};
function originalFunc(arg1, arg2) { // function body here } const copyFunc = function(...args) { return originalFunc.apply(this, args); };
function originalFunc(arg1, arg2) {
    // function body here
}
const copyFunc = function(...args) {
    return originalFunc.apply(this, args);
};

در این مثال،

copyFunc
copyFuncیک تابع جدید است که
originalFunc
originalFuncرا با همان آرگومان‌ها فراخوانی می‌کند. اما باید این موضوع را به خاطر داشته باشیم که این کار فقط یک کپی سطحی از تابع ایجاد می‌کند. هر تابع یا آبجکت مورد استفاده در
originalFunc
originalFuncهمچنان به مقادیر اصلی reference خواهند داشت.

همانطور که می‌بینیم، ایجاد کپی از آبجکت‌ها، آرایه‌ها و توابع می‌تواند یک تکنیک مفید در برنامه نویسی جاوااسکریپت باشد. استفاده از روش مناسب برای نوع داده‌ای که با آن کار می‌کنیم کمک می‌کند تا اطمینان حاصل کنیم که کد ما مطابق انتظاری که داریم عمل می‌کند و در طولانی مدت قابل نگه‌داری می‌باشد.

مقایسه کپی‌های سطی و عمیق

در زبان برنامه نویسی جاوااسکریپت دو راه برای کپی کردن آبجکت‌ها و آرایه ها وجود دارد: کپی سطحی (shallow copy) و کپی عمیق (deep copy). درک تفاوت بین این دو نوع کپی مهم است، زیرا می‌تواند بر رفتار کد ما تأثیر بگذارد.

یک کپی سطحی یک آبجکت یا آرایه جدید ایجاد می‌کند، اما فقط referenceها به ویژگی‌های آبجکت یا آرایه اصلی را کپی می‌کند.

به عبارت دیگر، آبجکت یا آرایه جدید دارای مقادیر مشابهی برای ویژگی‌های خود است، اما خود ویژگی‌ها همچنان به همان مقادیر در حافظه اشاره می‌کنند. این بدان معنی است که هر تغییری که در ویژگی‌های آبجکت یا آرایه جدید ایجاد شود، آبجکت یا آرایه اصلی را نیز تحت تأثیر قرار می‌دهد و بالعکس.

در ادامه یک مثال برای بررسی کپی سطحی از یک آرایه را بررسی می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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]]
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]]
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
originalArrاستفاده می‌شود. سپس، اولین المنت
shallowCopyArr
shallowCopyArrبه
۱۰
۱۰تغییر می‌کند که بر روی
originalArr
originalArrتأثیری نمی‌گذارد. اما وقتی اولین المنت آرایه تو در تو در
shallowCopyArr
shallowCopyArrبه
۴۰
۴۰تغییر می‌کند، در آرایه
originalArr
originalArrنیز تغییر اتفاق می‌افتد، زیرا هر دو آرایه به یک آرایه تو در تو reference دارند.

از طرف دیگر، کپی عمیق یک آبجکت یا آرایه جدید ایجاد می‌کند. همچنین مقادیر ویژگی‌های آبجکت یا آرایه اصلی را به جای referenceها کپی می‌کند. این بدان معنی است که هر تغییری که در آبجکت یا آرایه جدید ایجاد شود، آبجکت یا آرایه اصلی را تحت تأثیر قرار نمی‌دهد و بالعکس.

در در ادامه یک نمونه از کپی عمیق از یک آرایه را بررسی می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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]]
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]]
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
originalArrبه رشته از
JSON.stringify()
JSON.stringify()استفاده می‌کنیم، سپس برای تبدیل مجدد رشته به آرایه
JSON.parse()
JSON.parse()را به کار می‌گیریم. این کار یک آرایه جدید با مقادیر مشابه
originalArr
originalArrایجاد می‌کند، اما آرایه جدید کاملاً مستقل از آرایه اصلی است.

در نتیجه، تفاوت اصلی بین یک کپی سطحی و کپی عمیق در جاوااسکریپت این است که آیا آبجکت یا آرایه جدید فقط referenceها به ویژگی‌های آبجکت یا آرایه اصلی را کپی می‌کند یا اینکه مقادیر این ویژگی‌ها را نیز شامل می‌شود.

جمع‌بندی

به طور خلاصه، درک تفاوت بین مقادیر primitive و reference در جاوااسکریپت برای نوشتن کد کارآمد و بدون اشکال ضروری است. با آگاهی از نحوه ذخیره و دستکاری داده‌ها، می‌توانیم از رفتارهای غیرمنتظره جلوگیری کنیم و عملکرد برنامه‌های خود را بهبود بخشیم.

نکته‌ای که باید به یاد داشته باشیم این است که در جاوااسکریپت مقادیر تایپ‌های primitive همراه با value ارسال می‌شوند، در حالی که آبجکت‌ها و آرایه‌ها با reference ارسال می‌گردند. همچنین این موضوع را باید هنگام کار با توابع و تخصیص متغیرها نیز در نظر داشته باشیم.

شایان ذکر است که ECMAScript 6 یک کلمه کلیدی جدید به نام let معرفی کرد که بیشتر شبیه زبان‌های برنامه نویسی سنتی در رابطه با انتساب متغیر عمل می‌کند. let به ما این امکان را می‌دهد که یک متغیر با محدوده بلاک تعریف کنیم، به این معنی که متغیر فقط در داخل بلاک کدی که در آن تعریف شده است قابل دسترسی می‌باشد. این موضوع می‌تواند با محدود کردن دامنه متغیر، از ایجاد سردرگمی جلوگیری نماید.

در پایان، در حالی که مقادیر primitive و reference ممکن است جزییات کوچکی در طرح بزرگ برنامه نویسی جاوااسکریپت به نظر برسند، اما می‌توانند تأثیر قابل توجهی بر رفتار و کارایی کد ما داشته باشند. با درک تفاوت‌ها و استفاده از تکنیک‌های مناسب برای موقعیت خاص خود، می‌توانیم کدهای تمیزتر، مؤثرتر با خطای کم‌تر بنویسیم.

دیدگاه‌ها:

افزودن دیدگاه جدید