یکی از تفاوتهای اساسی بین شیءها و مقدارهای اصلی(primitives) این است که شیءها “توسط مرجع” ذخیره و کپی میشوند، در حالی که مقدارهای اصلی مانند رشتهها، اعداد، مقدارهای boolean و غیره، همیشه “به عنوان یک مقدار کلی” ذخیره میشوند.
اگر ما بدانیم زمانی که مقداری را کپی میکنیم چه اتفاقی میافتد، این موضوع را بهتر متوجه میشویم.
بیایید با یک مقدار اصلی مانند رشته شروع کنیم.
اینجا ما یک کپی از message
را درون phrase
قرار میدهیم:
let message = "Hello!";
let phrase = message;
در نتیجه ما دو متغیر مستقل داریم که هر کدام رشتهی "Hello!"
را ذخیره میکنند.
نتیجه خیلی بدیهی است نه؟
شیءها اینگونه نیستند.
متغیری که یک شیء به آن تخصیص داده شده باشد خود شیء را ذخیره نمیکند، بلکه “آدرس آن در حافظه” را ذخیره میکند. به عبارتی دیگر “یک مرجع” را ذخیره میکنند.
بیایید به مثالی از چنین متغیری نگاه کنیم:
let user = {
name: "John"
};
اینکه در واقع چگونه ذخیره میشود را اینجا گفتیم:
شیء در جایی از حافظه ذخیره شده است (سمت راست تصویر)، در حالی که متغیر user
(سمت چپ) به شیء “رجوع میکند”.
میتوانیم به متغیری که شیءای را ذخیره میکند، مانند user
، به عنوان یک ورق کاغذ که شامل آدرس شیء است نگاه کنیم.
زمانی که ما با شیء کاری انجام میدهیم، برای مثال یک ویژگی را میگیریم user.name
، موتور جاوااسکریپت به آدرس نگاه میکند که چه چیزی درون آن قرار دارد و عملیات را روی شیء واقعی انجام میدهد.
حال دلیل اهمیت آن اینجا آمده است.
زمانی که یک متغیر حاوی شیء کپی میشود، مرجع آن کپی شدهاست نه خود شیء.
برای مثال:
let user = { name: "John" };
let admin = user; // کپی شدن مرجع
حالا ما دو متغیر داریم که هر کدام یک مرجع به شیء یکسان را ذخیره میکنند:
همانطور که میبینید، هنوز یک شیء وجود دارد، اما حالا دو متغیر داریم که به آن رجوع میکنند.
برای دسترسی به شیء و تغییر محتوای آن میتوانیم از هر دو متغیر استفاده کنیم:
let user = { name: 'John' };
let admin = user;
admin.name = 'Pete'; // "admin" تغییر داده شده توسط مرجع
alert(user.name); // 'Pete' :هم قابل مشاهده هستند "user" تغییرات توسط مرجع
درست مانند این است که ما یک کمد با دو کلید داشته باشیم و با استفاده از یکی از کلیدها (admin
) آن را باز کنیم و درون آن تغییراتی انجام دهیم. سپس، اگر بعدا از کلید دیگر (user
) استفاده کردیم، هنوز هم کمد یکسانی را باز کردهایم و به محتوای تغییر داده شده دسترسی داریم.
مقایسه توسط مرجع
دو شیء تنها در حالتی که یک شیء یکسان باشند برابر هستند.
برای مثال، اینجا a
و b
به یک شیء یکسان رجوع میکنند، بنابراین برابر هستند:
let a = {};
let b = a; // کپی کردن مرجع
alert( a == b ); // true :هر دو متغیر به شیء یکسان رجوع میکنند پس
alert( a === b ); // true
در کد پایین دو شیء مستقل داریم و با اینکه مشابه بنظر میرسند اما برابر نیستند (هر دو خالی هستند):
let a = {};
let b = {}; // دو شیء مستقل
alert( a == b ); // false
برای مقایسههایی مانند obj1 > obj2
یا مقایسه شیء با یک مقدار اصلی obj == 5
، شیءها به مقدارهای اصلی تبدیل میشوند. ما چگونگی تبدیل شیءها را به زودی مطالعه میکنیم، اما اگر بخواهیم حقیقت را بگوییم، چنین تبدیلهایی به ندرت نیاز میشوند – آنها معمولا به عنوان نتیجهی یک اشتباه برنامهنویسی ظاهر میشوند.
یک عارضه جانبی مهم ذخیره شیءها به عنوان مرجع این است که شیءای که به عنوان const
تعریف شده است میتواند تغییر داده شود.
برای مثال:
const user = {
name: "John"
};
user.name = "Pete"; // (*)
alert(user.name); // Pete
شاید به نظر برسد که خط (*)
باعث ارور شود اما اینطور نیست. مقدار user
ثابت است و همیشه باید به شیءای یکسان رجوع کند اما ویژگیهای آن شیء برای تغییر آزاد هستند.
به عبارتی دیگر، const user
تنها اگر ما برای تنظیم user=...
تلاش کنیم ارور میدهد.
با این حال اگر واقعا نیاز داریم که ویژگیهای شیء را ثابت نگه داریم میتوانیم این کار را انجام دهیم اما با استفاده از متدهای کاملا متفاوت. ما این موضوع را در فصل پرچمهای ویژگی و توصیفکنندهها ذکر کردهایم.
کپی و ادغام کردن، Object.assign
پس کپی کردن یک متغیر حاوی شیء باعث ساخت یک مرجع اضافی به همان شیء میشود.
اما اگر ما نیاز داشته باشیم که چند نسخه از یک شیء بسازیم چه کار کنیم؟
میتوانیم یک شیء جدید بسازیم و ساختار شیءای که از قبل موجود است را با حلقه زدن بین ویژگیهای آن و کپی کردن آنها در سطح مقدارهای اصلی، در شیء جدید کپی کنیم.
مانند این کد:
let user = {
name: "John",
age: 30
};
let clone = {}; // شیء خالی جدید
// را درون آن کپی کنیم user بیایید تمام ویژگیهای
for (let key in user) {
clone[key] = user[key];
}
// حال شیء کپی شده یک شیء کاملا مستقل با محتوای یکسان است
clone.name = "Pete"; // تغییر دادن دادهی درون آن
alert( user.name ); // است John هنوز در شیء اصلی برابر با
همچنین ما میتوانیم از متد Object.assign برای این کار استفاده کنیم.
سینتکس آن اینگونه است:
Object.assign(dest, ...sources)
- اولین آرگومان
dest
همان شیء مقصود است. - آرگومانهای بعدی
src1, ..., srcN
(ممکن است هر تعدادی باشد) شیءها منبع هستند. - این متد ویژگیهای تمام شیءها منبع
src1, ..., srcN
را درونdest
کپی میکند. به عبارتی دیگر، ویژگیهای تمام آرگومانهای بعد از دومین آرگومان، درون شیء اول کپی میشوند. - متد صدازده شده
dest
را برمیگرداند.
برای مثال، میتوانیم از این متد برای ادغام چند شیء و ریختن آنها درون یک شیء استفاده کنیم:
let user = { name: "John" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// کپی میکند user درون permissions2 و permissions1 تمام ویژگیها را از
Object.assign(user, permissions1, permissions2);
<<<<<<< HEAD
// user = { name: "John", canView: true, canEdit: true } :حالا داریم
alert(user.name); // John
alert(user.canView); // true
alert(user.canEdit); // true
اگر ویژگی کپیشده از قبل وجود داشته باشد، دوباره مقداردهی میشود:
let user = { name: "John" };
Object.assign(user, { name: "Pete" });
alert(user.name); // user = { name: "Pete" } :حالا داریم
همچنین میتوانیم برای کپی کردنهای ساده از Object.assign
استفاده کنیم:
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
alert(clone.name); // John
alert(clone.age); // 30
تمام ویژگیهای user
درون شیء خالی کپی و برگردانده میشوند.
همچنین متدهای دیگری برای کپی یک شیء وجود دارد مانند استفاده کردن از سینتکس spread clone = {...user}
که بعدا در این آموزش پوشش داده میشود.
کپی کردن تو در تو
تا اینجا ما فرض کردیم که تمام ویژگیهای user
مقدارهای اصلی هستند. اما ویژگی ها میتوانند به شیءهای دیگر رجوع کنند.
مانند این کد:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
اینجا کپی کردن clone.sizes = user.sizes
کافی نیست، چون user.sized
یک شیء است و توسط مرجع کپی میشود. پس clone
و user
سایزهای یکسانی را مشترک میشوند:
مثل این:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = Object.assign({}, user);
alert( user.sizes === clone.sizes ); // true :شیءهای یکسان پس
<<<<<<< HEAD
// سایزهای مشترک دارند clone و user
user.sizes.width = 60; // یک ویژگی را از یک جا تغییر دهید
alert(clone.sizes.width); // 60 :نتیجه را از جای دیگر ببینید
برای رفع این اشکال و مجبور کردن user
و clone
به اینکه واقعا مجزا باشند، ما باید از یک حلقهی کپیکردن استفاده کنیم که هر مقدار user[key]
را بررسی میکند و اگر شیء بود، سپس ساختار آن را هم کپی میکند. به این کار “کپیکردن عمیق” یا “کپیکردن ساختاری” میگویند. یک متد structuredClone وجود دارد که کپیکردن عمیق را پیادهسازی میکند.
متد structuredClone
فراخوانی structuredClone(object)
شیء object
با تمام ویژگیهای تودرتو را کپی میکند.
اینجا نشان میدهیم چگونه میتوانیم از آن در مثال خود استفاده کنیم:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = structuredClone(user);
alert( user.sizes === clone.sizes ); // false ،شیءهای متفاوت
// اکنون کاملا نامرتبط هستند clone و user شیءهای
user.sizes.width = 60; // ویژگیای را از جایی تغییر دهید
alert(clone.sizes.width); // 50 ،مرتبط نیستند
متد structuredClone
میتواند اکثر انواع داده مانند شیءها، آرایهها و مقدارهای اصلی را کپی کند.
این متد همچنین مرجعهای دایرهای را هم پوشش میدهد، زمانی که یک ویژگی شیء به خود شیء رجوع میکند (به صورت مستقیم یا از طریق یک زنجیره از مرجعها).
برای مثال:
let user = {};
// :بیایید یک مرجع دایرهای بسازیم
// رجوع میکند user به خود user.me
user.me = user;
let clone = structuredClone(user);
alert(clone.me === clone); // true
همانطور که میتوانید ببینید، clone.me
به clone
رجوع میکند نه به user
! پس مرجع دایرهای هم به درستی کپی شده.
اگرچه، مواردی وجود دارند که structuredClone
موفقیتآمیز نیست.
برای مثال، زمانی که یک شیء ویژگی تابع داشته باشد:
// ارور
structuredClone({
f: function() {}
});
ویژگیهای تابع پشتیبانی نمیشوند.
برای مدیریت چنین مواردی ما شاید نیاز داشته باشیم که از ترکیبی از متدهای کپیسازی استفاده کنیم، کد شخصیسازی شده بنویسیم یا برای اینکه چرخ را دوباره نسازیم، یک پیادهسازی موجود را استفاده کنیم، برای مثال _.cloneDeep(obj) از کتابخانه lodash جاوااسکریپت.
خلاصه
شیءها توسط مرجع تخصیص داده و کپی میشوند. به عبارتی دیگر، یک متغیر “مقدار حاوی شیء” را دخیره نمیکند، بلکه یک “مرجع” (آدرس درون حافظه) را به عنوان مقدار ذخیره میکند. پس کپی کردن چنین متغیری یا رد کردن آن به عنوان یک آرگومان تابع آن مرجع را کپی میکند نه خود شیء را.
تمام عملیاتها (مانند اضافه/کم کردن ویژگیها) از طریق مرجع کپیشده روی شیء یکسان انجام میشوند.
برای اینکه یک “کپی واقعی” (یک مشبه) را بسازیم میتوانیم از Object.assign
برای “کپیهای سطحی” (شیءهای تو در تو توسط مرجع کپی میشوند) یا از یک تابع «کپیسازی عمیق» structuredClone
یا یک پیادهسازی شخصیسازی شده مانند _.cloneDeep(obj) استفاده کنیم.