۱۲ اکتبر ۲۰۲۲

مراجع شیء و کپی کردن

یکی از تفاوت‌های اساسی بین شیءها و مقدارهای اصلی(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 تعریف شده است می‌تواند تغییر داده شود.

برای مثال:

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) استفاده کنیم.

نقشه آموزش