مقدمة: هل سبق لك أن وقعت في فخ النسخ؟
كمطور ويب، قد تواجه أحياناً سلوكاً محيراً عند محاولة نسخ الكائنات في JavaScript. قد تقوم بنسخ كائن، ثم تعدّل النسخة، لتتفاجأ بأن الكائن الأصلي قد تغير أيضاً! ما الذي يحدث هنا؟ هذا الدرس سيأخذك في رحلة لفهم جوهر مشكلة النسخ في JavaScript، وكيف تختلف أنواع البيانات في طريقة تعاملها مع عملية النسخ.
النسخ في JavaScript: المرجع مقابل القيمة
في JavaScript، تنقسم البيانات إلى نوعين رئيسيين، وكلاهما يتعامل مع عملية النسخ بطريقة مختلفة تماماً:
1. النسخ بالقيمة (لأنواع البيانات البدائية - Primitive Types)
أنواع البيانات البدائية تشمل الأرقام (Numbers)، السلاسل النصية (Strings)، القيم المنطقية (Booleans)، null، undefined، و Symbol، و BigInt. عند نسخ هذه الأنواع، يتم إنشاء نسخة مستقلة تماماً من القيمة. أي تغيير على النسخة لا يؤثر على الأصل.
let originalNumber = 10;
let copiedNumber = originalNumber; // نسخ بالقيمة
copiedNumber = 20; // تعديل النسخة
console.log("الرقم الأصلي:", originalNumber); // النتيجة: 10 (لم يتغير)
console.log("الرقم المنسوخ:", copiedNumber); // النتيجة: 20
كما ترى، تغيير copiedNumber لم يؤثر على originalNumber. هذا هو السلوك المتوقع والآمن.
2. النسخ بالمرجع (للكائنات - Objects)
الكائنات (Objects) تشمل الكائنات العادية ({})، المصفوفات (Arrays)، الدوال (Functions)، والعديد من الأنواع المدمجة الأخرى. عندما تقوم بنسخ كائن، فإنك لا تنسخ الكائن نفسه، بل تنسخ مرجعاً (Reference) إلى نفس المكان الذي يخزن فيه الكائن في الذاكرة. هذا يعني أن كلا المتغيرين يشيران إلى نفس الكائن الحقيقي.
المشكلة الحقيقية: تعديل النسخة يؤثر على الأصل!
دعنا نرى كيف تتجلى هذه المشكلة باستخدام الكود الذي لدينا:
const user = { name: "أحمد", details: { age: 25, city: "الرياض" } };
// هذا ليس نسخاً حقيقياً! إنه مجرد نسخ للمرجع.
const wrongCopy = user;
// لنقم بتعديل "النسخة" الجديدة
wrongCopy.name = "سارة";
console.log("المستخدم الأصلي:", user.name); // النتيجة: سارة! (تغير الأصل)
console.log("النسخة الخاطئة:", wrongCopy.name); // النتيجة: سارة
كما ترى بوضوح، بتعديل wrongCopy.name، تغير اسم المستخدم الأصلي (user.name) أيضاً ليصبح "سارة"! هذا لأن wrongCopy لم تنشئ كائناً جديداً ومستقلاً، بل أشارت إلى نفس الكائن الذي يشير إليه user. أي تعديل يتم عبر أي من المتغيرين سيؤثر على الكائن الفعلي المشترك بينهما.
ماذا عن النسخ السطحي (Shallow Copy)؟
قد تكون على دراية بطرق مثل عامل الانتشار (Spread Operator ...) أو الدالة Object.assign(). هذه الطرق تقوم بما يسمى النسخ السطحي (Shallow Copy). هذا يعني أنها تنسخ المستوى الأول من الكائن بالقيمة، ولكن إذا كان الكائن الأصلي يحتوي على كائنات متداخلة (Nested Objects)، فإنها تنسخ هذه الكائنات المتداخلة بالمرجع.
حدود النسخ السطحي مع الكائنات المتداخلة:
const user = { name: "أحمد", details: { age: 25, city: "الرياض" } };
// إنشاء نسخة سطحية باستخدام عامل الانتشار
const shallowCopy = { ...user };
// تعديل خاصية في المستوى الأول (تعمل بشكل صحيح)
shallowCopy.name = "فاطمة";
// تعديل خاصية داخل كائن متداخل (ستؤثر على الأصل!)
shallowCopy.details.age = 30;
console.log("المستخدم الأصلي:", user.name, user.details.age); // النتيجة: أحمد، 30 (العمر تغير!)
console.log("النسخة السطحية:", shallowCopy.name, shallowCopy.details.age); // النتيجة: فاطمة، 30
هنا، تغيير shallowCopy.name لم يؤثر على user.name (وهو أمر جيد!). ولكن عندما غيرنا shallowCopy.details.age، تأثر user.details.age أيضاً! هذا لأن الكائن المتداخل details تم نسخه بالمرجع، وليس بالقيمة، مما يعني أن user.details و shallowCopy.details يشيران إلى نفس الكائن في الذاكرة.
الحاجة إلى النسخ العميق (Deep Copy)
إذاً، ما الحل إذا أردنا نسخة مستقلة تماماً من الكائن، بما في ذلك جميع الكائنات المتداخلة بداخله، بحيث لا يؤثر أي تعديل على النسخة في الأصل؟ هنا يأتي دور النسخ العميق (Deep Copy).
الخلاصة والخطوة التالية
فهم الفرق بين النسخ بالمرجع والنسخ بالقيمة أمر بالغ الأهمية لتجنب الأخطاء غير المتوقعة في تطبيقاتك. لقد رأينا أن النسخ السطحي لا يكفي للتعامل مع الكائنات المتداخلة. في الدرس القادم، سنتعرف على الحل الأمثل والحديث لهذه المشكلة: الدالة structuredClone()، وكيف يمكنها أن تجعل حياتك كمطور أسهل بكثير!
🔗 الخطوة التالية: انتقل إلى التطبيق العملي وجرب الكود بنفسك من هنا.