٢٣ مايو ٢٠٢٣

Object references and copying

أحد الاختلافات الأساسية بين الكائنات والقيم الأساسية هو أن الكائنات تخزن وتنسخ “ضمنيًا” وبالمقابل، يتم نسخ القيم الأساسية بالكامل.

هذا سهل الفهم إذا نظرنا قليلاً تحت الغطاء لما يحدث عند نسخ القيمة.

لنبدأ بالقيمة الأساسية، مثل السلسلة.

هنا نضع نسخة من message في phrase:

let message = "Hello!";
let phrase = message;

نحن الآن لدينا متغيران مستقلان، يخزن كل منهما السلسانة "Hello!".

نتيجة واضحة جدًا، أليس كذلك؟

الكائنات ليست كذلك.

يخزن المتغير الذي يشير إلى كائن ليس الكائن نفسه، ولكن “عنوانه في الذاكرة” – بعبارة أخرى “مرجعًا” له.

لنلقي نظرة على مثال لمتغير من هذا النوع:

let user = {
    name: "John",
};

وهنا كيف يتم تخزينه في الذاكرة:

يتم تخزين الكائن في مكان ما في الذاكرة (على اليمين من الصورة)، في حين يحتوي المتغير user (على اليسار) على “مرجع” إلى هذا المكان.

يمكننا التفكير في متغير الكائن، مثل user، كورقة ورق بها عنوان الكائن.

عندما نقوم بإجراء أي عمليات على الكائن، مثل الوصول إلى خاصية user.name، يقوم محرك JavaScript بالنظر إلى ما يوجد في ذلك العنوان وينفذ العملية على الكائن الفعلي.

والآن هنا هو السبب في أهمية هذا الأمر.

عند نسخ متغير الكائن ، يتم نسخ المرجع ، ولكن الكائن نفسه لا يتم استنساخه.

على سبيل المثال:

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) في وقت لاحق، فنحن لا نفتح سوى نفس الخزانة ويمكننا الوصول إلى المحتويات المغيرة.

المقارنة بالمؤشرات

Two objects are equal only if they are the same object.

For instance, here a and b reference the same object, thus they are equal:

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، يتم تحويل الكائنات إلى قيم أولية. سندرس كيفية عمل تحويلات الكائنات قريبًا جدًا، ولكن لنقل الحقيقة، تحتاج هذه المقارنات نادرًا جدًا – عادة ما تظهر نتيجة خطأ في البرمجة.

استنساخ ودمج، 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 لذلك.

The syntax is:

Object.assign(dest, [src1, src2, src3...])
  • المعامل الأول dest هو الكائن المراد.
  • باقي المعاملات src1, ..., srcN (يمكن أن تكون أي عدد) هي المصادر المراد نسخها.
  • تقوم بنسخ خواص المصادر src1, ..., srcN إلى الهدف dest. بكلمات أخرى يتم نسخ الخواص من كل المعاملات بدءًا من الثاني ويتم وضعها في الأول.
  • وترجع dest.

مثلًا يمكننا استخدامها لدمج العديد من الكائنات في كائن واحد:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// نسخ كل الخواص من permissions1 و permissions2 إلى user
Object.assign(user, permissions1, permissions2);

// now user = { name: "John", canView: true, canEdit: true }

إذا كانت الخاصية موجودة يتم استبدالها:

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // now user = { name: "Pete" }

إيضًا يمكننا استخدام Object.assign لاستبدال الحلقة التكرارية for..in في النسخ البسيط:

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

تنسخ كل الخواص من user إلى كائن فارغ وترجعه.

النسخ المتداخل

هناك أيضًا طرق أخرى لاستنساخ كائن، على سبيل المثال باستخدام spread syntax clone = {...user}، والتي سيتم تغطيتها في وقت لاحق في البرنامج التعليمي.

النسخ الشامل

مثل هذا:

let user = {
    name: "John",
    sizes: {
        height: 182,
        width: 50,
    },
};

alert(user.sizes.height); // 182

الآن ليس كافيًا نسخ clone.sizes = user.sizes لأن user.sizes هو كائن وسيتم نسخ المؤشر ويكون clone و user لهما نفس الخاصية sizes:

مثل هذا:

let user = {
    name: "John",
    sizes: {
        height: 182,
        width: 50,
    },
};

let clone = Object.assign({}, user);

alert(user.sizes === clone.sizes); // true, نفس الكائن

// user و clone يتشاركان sizes
user.sizes.width++; // تغيير الخاصية من مكان
alert(clone.sizes.width); // 51, يجعل التغيير مئي في المكان الآخر

لحل هذه المشكلة، يجب علينا استخدام حلقة نسخ تفحص كل قيمة في user[key] وإذا كانت كائنًا، فإنه يتم تكرار هيكله أيضًا. يُسمى ذلك “نسخًا عميقًا”.

يمكن استخدام التكرار لتنفيذ نسخة عميقة، أو يمكن استخدام تنفيذ موجود لعدم اختراع العجلة، على سبيل المثال _.cloneDeep(obj) من مكتبة JavaScript lodash.

يمكن تعديل الكائنات التي تم تعريفها بـ const

آثار جانبية مهمة لتخزين الكائنات كمراجع هو أنه يمكن تعديل كائن معرف بـ const.

على سبيل المثال:

const user = {
  name: "John"
};

user.name = "Pete"; // (*)

alert(user.name); // Pete

قد يبدو أن السطر (*) سيتسبب في حدوث خطأ، ولكن لا يحدث خطأ. قيمة user ثابتة، يجب أن يشير دائمًا إلى نفس الكائن، ولكن يمكن تغيير خصائص هذا الكائن.

بمعنى آخر، يتم إصدار خطأ عند استخدام const user فقط إذا حاولنا تعيين user=... ككل.

ومع ذلك، إذا كنا بحاجة فعلًا إلى جعل خصائص الكائن ثابتة، فمن الممكن ذلك باستخدام طرق مختلفة تمامًا. سنذكر ذلك في الفصل رايات الخصائص و واصفاتها.

ملخص

الكائنات تتم تعيينها ونسخها بالمرجع. وبمعنى آخر، يخزن المتغير ليس “قيمة الكائن” ولكن “مرجع” (عنوان في الذاكرة) للقيمة. لذلك، عند نسخ هذا المتغير أو تمريره كوسيط لدالة، يتم نسخ هذا المرجع، وليس الكائن نفسه.

كل العمليات التي تتم بواسطة النسخة (مثل إضافة وحذف الخواص) تحدث على نفس الكائن.

لعمل نسخة حقيقية يمكننا استخدام Object.assign لما يسمى “shallow copy” (الكائنات الداخلية تنسخ بالمؤشر) أو دالة “deep cloning” مثل _.cloneDeep(obj).

خريطة الدورة التعليمية