اذهب إلى المحتوى

نسخ الكائنات بالمرجع (by reference) في جافاسكربت


Mohamed Lahlah

أحد الاختلافات الأساسية بين الكائنات (objects) وأنواع البيانات الأولية (primitives) هو تخزينها ونسخها "بالطريقة المرجعية" (by reference).

قيم أنواع البيانات الأولية: هي سلاسل وأرقام وقيم منطقية - تُسند أو تنسخ "كقيمة كاملة".

فمثلًا:

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

نتيجة لتنفيذ الشيفرة السابقة لدينا متغيرين مستقلين، كلّ واحد يُخزن السلسلة "Hello!‎".

variable-copy-value.png

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

لا يخزن المتغيّر الكائن نفسه، وإنما "عنوانه في الذاكرة"، بمعنى آخر "مرجع" له.

هذا هو وصف الكائن:

let user = {
  name: "John"
};

 

variable-contains-reference.png

هنا يُخزن الكائن في مكان ما في الذاكرة. والمتغير user له "مرجع" له.

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

فمثلًا:

let user = { name: "John" };

let admin = user; // نسخ المرجع

الآن لدينا متغيرين، كل واحد له إشارة مرجعية لنفس الكائن:

variable-copy-reference.png

يمكننا استخدام أي متغيّر للوصول إلى الكائن وتعديل محتوياته:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // غيرت القيمة من خلال المتغير ‫admIn المرجعي

alert(user.name); // ‫'Pete' ظهر التغيير على المتغير "user" المرجعي

يوضح المثال أعلاه أن هناك كائنًا واحدًا فقط. كما لو كان لدينا خزانة لها مفتاحان واستخدمنا أحدهما وهو (admin) للوصول إليها. بعد ذلك، إذا استخدمنا لاحقًا مفتاحًا آخر وهو (user)، فيمكننا رؤية التغييرات.

الموازنة بحسب المرجع

إن معاملات المساواة == والمساواة الصارمة === الخاصة بالكائنات تعمل بأسلوب واحد.

الكائنين يكونان متساويان فقط إذا كانا يشيران لنفس الكائن.

هنا يشير المتغيرين لنفس الكائن، وبالتالي فإنهما متساويان:

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

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

يؤدي نسخ متغير يخزن كائن إلى إنشاء مرجع آخر لنفس الكائن.

ولكن ماذا لو احتجنا إلى تكرار كائنٍ ما؟ إنشاء نسخة مستقلة تمامًا، أي استنساخه؟

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

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

هكذا:

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

let clone = {}; // كائن فارغ جديد

// ‫لننسخ كلّ الخاصيات الموجودة في user إليه
for (let key in user) {
  clone[key] = user[key];
}

// الآن الكائن ‫clone هو نسخة مستقلة تمامًا لديه نفس خاصيات الكائن user
clone.name = "Pete"; // غيرنا البيانات بداخله

alert( user.name ); // ما يزال الاسم ‫John في الكائن الأساسي

كما يمكننا استخدام الطريقة Object.assign لذلك.

وتكون صياغتها هكذا:

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 في الكائن الفارغ ويُعاد.

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

حتى الآن افترضنا أن جميع خصائص الكائن user تكون قيم أولية. لكن ماذا عن الخصائص يمكن أن تكون مراجع لكائنات أخرى؟ كيف سنتعامل معها؟

هكذا:

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

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

الآن لا يكفي نسخ clone.sizes = user.sizes، لأن user.sizes كائن، سينسخه بالطريقة المرجعية. لذا فإن clone وuser سيتشاركان نفس الأحجام:

هكذا:

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

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

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

// إن ‫user و clone لديهما نفس الحجم 
user.sizes.width++;       // غير خاصية من مكان معين
alert(clone.sizes.width); // ‫51, تفقد هل الخاصية تغيرت في المكان الآخر (بالتأكيد)

لإصلاح ذلك، يجب علينا استخدام حلقة الاستنساخ والّتي تفحص كلّ قيمة من قيم user[key]‎، فإذا كانت كائنًا، فعندها تُكرر هيكلها أيضًا. وهذا ما يسمى الاستنساخ العميق (Deep Cloning).

هناك خوارزمية قياسية للاستنساخ العميق تتعامل مع الحالة المذكورة أعلاه وحالات أكثر تعقيدًا، تسمى خوارزمية الاستنساخ المهيكل (Structured cloning algorithm).

كما يمكننا أيضًا استخدام العودية لتنفيذها. أو ببساطة ليس علينا إعادة اختراع العجلة، إذ يمكننا أخذ تطبيق جاهز لهذا الأمر، مثل التابع ‎_.cloneDeep(obj)‎ الموجود في مكتبة Lodash.

الخلاصة

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

تُنفذُ جميع العمليات من خلال المراجع المنسوخة (مثل إضافة / إزالة الخاصيات) على نفس الكائن الفردي.

لعمل "نسخة حقيقية" (نسخة مستقلة)، يمكننا استخدام Object.assign لما يسمى "بالنسخة السطحية" (إذ تُنسخ الكائنات المُتداخلة بالطريقة المرجعية) أو يمكننا النسخ من خلال دالّة "الاستنساخ العميق"، مثل الدالّة ‎_.cloneDeep(obj)‎.

ترجمة -وبتصرف- للفصل Object copying, references من كتاب The JavaScript language


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...