أحد الاختلافات الأساسية بين الكائنات والأوليات هو أن الكائنات يتم تخزينها ونسخها "بالرجوع إليها"، في حين أن القيم الأولية: السلاسل والأرقام والقيم المنطقية وما إلى ذلك - يتم نسخها دائمًا "كقيمة كاملة".
من السهل أن نفهم ذلك إذا نظرنا قليلاً إلى ما يحدث عندما نقوم بنسخ قيمة.
لنبدأ ببدائية، مثل سلسلة.
هنا نضع نسخة من message
في phrase
:
دع الرسالة = "مرحبًا!"; دع العبارة = رسالة؛
ونتيجة لذلك، أصبح لدينا متغيرين مستقلين، يقوم كل منهما بتخزين السلسلة النصية "Hello!"
.
نتيجة واضحة تماما، أليس كذلك؟
الكائنات ليست هكذا.
المتغير المخصص لكائن ما لا يخزن الكائن نفسه، بل "عنوانه في الذاكرة" - بمعنى آخر "مرجع" إليه.
دعونا نلقي نظرة على مثال لمثل هذا المتغير:
السماح للمستخدم = { الاسم: "جون" };
وإليك كيفية تخزينها فعليًا في الذاكرة:
يتم تخزين الكائن في مكان ما في الذاكرة (على يمين الصورة)، في حين أن متغير user
(على اليسار) لديه "مرجع" إليه.
قد نفكر في متغير كائن، مثل user
، مثل ورقة تحتوي على عنوان الكائن.
عندما نقوم بتنفيذ إجراءات مع الكائن، على سبيل المثال، الحصول على خاصية user.name
، فإن محرك JavaScript ينظر إلى ما هو موجود في هذا العنوان ويقوم بتنفيذ العملية على الكائن الفعلي.
الآن إليكم سبب أهميته.
عندما يتم نسخ متغير كائن، يتم نسخ المرجع، ولكن لا يتم تكرار الكائن نفسه.
على سبيل المثال:
السماح للمستخدم = { الاسم: "جون" }؛ دع المشرف = المستخدم؛ // انسخ المرجع
الآن لدينا متغيرين، كل منهما يخزن مرجعًا لنفس الكائن:
كما ترون، لا يزال هناك كائن واحد، ولكن الآن مع متغيرين يشيران إليه.
يمكننا استخدام أي من المتغيرين للوصول إلى الكائن وتعديل محتوياته:
السماح للمستخدم = { الاسم: 'جون' }؛ دع المشرف = المستخدم؛ admin.name = 'بيت'; // تم التغيير بواسطة مرجع "المسؤول". تنبيه (اسم المستخدم) ؛ // "بيت"، تتم رؤية التغييرات من مرجع "المستخدم".
يبدو الأمر كما لو كان لدينا خزانة بها مفتاحان واستخدمنا أحدهما ( admin
) للدخول إليها وإجراء التغييرات. ثم، إذا استخدمنا لاحقًا مفتاحًا آخر ( user
)، فإننا لا نزال نفتح نفس الخزانة ويمكننا الوصول إلى المحتويات التي تم تغييرها.
كائنان متساويان فقط إذا كانا نفس الكائن.
على سبيل المثال، هنا a
و b
يشيران إلى نفس الكائن، وبالتالي فهما متساويان:
دع = {}؛ دع ب = أ؛ // انسخ المرجع تنبيه (أ == ب)؛ // صحيح، كلا المتغيرين يشيران إلى نفس الكائن تنبيه (أ === ب)؛ // حقيقي
وهنا كائنان مستقلان غير متساويين، على الرغم من أنهما متشابهان (كلاهما فارغان):
دع = {}؛ دع ب = {}؛ // كائنين مستقلين تنبيه (أ == ب)؛ // خطأ شنيع
لإجراء مقارنات مثل obj1 > obj2
أو للمقارنة مع كائن بدائي obj == 5
، يتم تحويل الكائنات إلى عناصر أولية. سندرس كيفية عمل تحويلات الكائنات قريبًا جدًا، ولكن في الحقيقة، نادرًا ما تكون هناك حاجة لمثل هذه المقارنات - وعادةً ما تظهر كنتيجة لخطأ برمجي.
يمكن تعديل كائنات Const
أحد الآثار الجانبية المهمة لتخزين الكائنات كمراجع هو أنه يمكن تعديل الكائن الذي تم تعريفه على أنه const
.
على سبيل المثال:
مستخدم ثابت = { الاسم: "جون" }; user.name = "بيت"; // (*) تنبيه (اسم المستخدم) ؛ // بيت
قد يبدو أن السطر (*)
من شأنه أن يسبب خطأ، لكنه ليس كذلك. قيمة user
ثابتة، ويجب أن تشير دائمًا إلى نفس الكائن، ولكن خصائص هذا الكائن قابلة للتغيير.
بمعنى آخر، const user
يعطي خطأ فقط إذا حاولنا تعيين user=...
ككل.
ومع ذلك، إذا كنا حقًا بحاجة إلى إنشاء خصائص ثابتة للكائن، فمن الممكن أيضًا، ولكن باستخدام طرق مختلفة تمامًا. سنذكر ذلك في فصل علامات الخصائص وأوصافها.
لذلك، يؤدي نسخ متغير كائن إلى إنشاء مرجع آخر لنفس الكائن.
ولكن ماذا لو كنا بحاجة إلى تكرار كائن؟
يمكننا إنشاء كائن جديد وتكرار بنية الكائن الحالي، من خلال تكرار خصائصه ونسخها على المستوى البدائي.
مثله:
السماح للمستخدم = { الاسم: "جون"، العمر: 30 }; دع الاستنساخ = {}؛ // الكائن الفارغ الجديد // لننسخ كافة خصائص المستخدم فيه لـ (السماح بإدخال المستخدم) { clone[key] = user[key]; } // الآن يعد الاستنساخ كائنًا مستقلاً تمامًا له نفس المحتوى clone.name = "بيت"؛ // غيرت البيانات الموجودة فيه تنبيه (اسم المستخدم) ؛ // لا يزال جون في الكائن الأصلي
يمكننا أيضًا استخدام الطريقة Object.sign.
بناء الجملة هو:
Object.assis(وجهة, ...مصادر)
الوسيطة الأولى dest
هي كائن مستهدف.
الحجج الإضافية هي قائمة الكائنات المصدر.
يقوم بنسخ خصائص جميع الكائنات المصدر إلى dest
، ثم يُرجعها كنتيجة.
على سبيل المثال، لدينا كائن user
، فلنضيف إليه بعض الأذونات:
السماح للمستخدم = { الاسم: "جون" }؛ السماح للأذونات1 = {canView: true }; السماح للأذونات2 = {canEdit: true }; // نسخ جميع الخصائص من الأذونات 1 والأذونات 2 إلى المستخدم Object.assis(user,أذونات1,أذونات2); // الآن المستخدم = { الاسم: "جون"، canView: صحيح، canEdit: صحيح } تنبيه (اسم المستخدم) ؛ // جون تنبيه (user.canView)؛ // حقيقي تنبيه (user.canEdit)؛ // حقيقي
إذا كان اسم الخاصية المنسوخة موجودًا بالفعل، فسيتم استبداله:
السماح للمستخدم = { الاسم: "جون" }؛ Object.assis(user, { name: "بيت" }); تنبيه (اسم المستخدم) ؛ // الآن المستخدم = { الاسم: "بيت" }
يمكننا أيضًا استخدام Object.assign
لإجراء عملية استنساخ بسيطة للكائنات:
السماح للمستخدم = { الاسم: "جون"، العمر: 30 }; Let clone = Object.assis({}, user); تنبيه (clone.name)؛ // جون تنبيه (clone.age)؛ // 30
هنا يقوم بنسخ جميع خصائص user
إلى الكائن الفارغ وإعادته.
توجد أيضًا طرق أخرى لاستنساخ كائن، على سبيل المثال، استخدام صيغة الانتشار clone = {...user}
، والتي سيتم تناولها لاحقًا في البرنامج التعليمي.
حتى الآن افترضنا أن جميع خصائص user
بدائية. لكن الخصائص يمكن أن تكون إشارات إلى كائنات أخرى.
مثله:
السماح للمستخدم = { الاسم: "جون"، الأحجام: { الارتفاع: 182، العرض: 50 } }; تنبيه (user.sizes.height)؛ // 182
الآن لا يكفي نسخ clone.sizes = user.sizes
، لأن user.sizes
هو كائن، وسيتم نسخه حسب المرجع، لذلك سيتشارك clone
user
في نفس الأحجام:
السماح للمستخدم = { الاسم: "جون"، الأحجام: { الارتفاع: 182، العرض: 50 } }; Let clone = Object.assis({}, user); تنبيه (user.sizes === clone.sizes ); // صحيح، نفس الكائن // أحجام مشاركة المستخدم والاستنساخ user.sizes.width = 60; // تغيير خاصية من مكان واحد تنبيه (clone.sizes.width)؛ // 60، احصل على النتيجة من الآخر
لإصلاح ذلك وجعل user
clone
كائنين منفصلين حقًا، يجب علينا استخدام حلقة استنساخ تفحص كل قيمة user[key]
، وإذا كان كائنًا، فقم بتكرار بنيته أيضًا. وهذا ما يسمى "الاستنساخ العميق" أو "الاستنساخ المنظم". هناك طريقة StructuredClone التي تنفذ الاستنساخ العميق.
يستنسخ استدعاء structuredClone(object)
object
بجميع خصائصه المتداخلة.
وإليك كيف يمكننا استخدامه في مثالنا:
السماح للمستخدم = { الاسم: "جون"، الأحجام: { الارتفاع: 182، العرض: 50 } }; Let clone = StructuredClone(user); تنبيه (user.sizes === clone.sizes ); // خطأ، كائنات مختلفة // المستخدم والاستنساخ ليسا مرتبطين تمامًا الآن user.sizes.width = 60; // تغيير خاصية من مكان واحد تنبيه (clone.sizes.width)؛ // 50، غير مرتبط
يمكن لطريقة structuredClone
استنساخ معظم أنواع البيانات، مثل الكائنات والمصفوفات والقيم الأولية.
كما أنه يدعم المراجع الدائرية، عندما تشير خاصية الكائن إلى الكائن نفسه (مباشرة أو عبر سلسلة أو مراجع).
على سبيل المثال:
دع المستخدم = {}؛ // لنقم بإنشاء مرجع دائري: // يشير user.me إلى المستخدم نفسه user.me = user; Let clone = StructuredClone(user); تنبيه (clone.me === clone)؛ // حقيقي
كما ترون، يشير clone.me
إلى clone
، وليس user
! لذلك تم استنساخ المرجع الدائري بشكل صحيح أيضًا.
على الرغم من وجود حالات يفشل فيها structuredClone
.
على سبيل المثال، عندما يكون للكائن خاصية دالة:
// خطأ منظمكلون({ و: وظيفة () {} });
خصائص الوظيفة غير مدعومة.
للتعامل مع مثل هذه الحالات المعقدة، قد نحتاج إلى استخدام مجموعة من طرق الاستنساخ، أو كتابة تعليمات برمجية مخصصة، أو، حتى لا نعيد اختراع العجلة، نأخذ تطبيقًا موجودًا، على سبيل المثال _.cloneDeep(obj) من lodash مكتبة JavaScript.
يتم تعيين الكائنات ونسخها حسب المرجع. بمعنى آخر، لا يقوم المتغير بتخزين "قيمة الكائن"، بل "مرجع" (عنوان في الذاكرة) للقيمة. لذا فإن نسخ مثل هذا المتغير أو تمريره كوسيطة دالة ينسخ هذا المرجع، وليس الكائن نفسه.
يتم تنفيذ جميع العمليات عبر المراجع المنسوخة (مثل إضافة/إزالة الخصائص) على نفس الكائن الفردي.
لإنشاء "نسخة حقيقية" (استنساخ)، يمكننا استخدام Object.assign
لما يسمى "النسخة الضحلة" (يتم نسخ الكائنات المتداخلة حسب المرجع) أو وظيفة "الاستنساخ العميق" structuredClone
أو استخدام تطبيق استنساخ مخصص، مثل مثل _.cloneDeep(obj).