أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول
البرمجة بالعربي

Deep Copy و Shallow Copy في JavaScript: ليه تعديل النسخة بيغيّر الأصل

📅 ٢٢ مايو ٢٠٢٦⏱ 8 دقائق قراءة
Deep Copy و Shallow Copy في JavaScript: ليه تعديل النسخة بيغيّر الأصل

مستوى المقال: متوسط. الكلام موجّه لمن يكتب JavaScript بانتظام ويعرف الـ objects والـ arrays وجرّب الـ spread operator. لا تحتاج خبرة سابقة في إدارة الذاكرة أو الفرق بين الـ stack والـ heap؛ سنشرحهما بمثال بسيط قبل التعريف الدقيق.

Deep Copy و Shallow Copy في JavaScript: ليه تعديل النسخة بيغيّر الأصل

لو نسخت object بـ { ...obj } وافتكرت إنك خدت نسخة مستقلة، فانت على بُعد سطر واحد من bug صامت. هنا هتعرف بالظبط ليه تعديل النسخة بيغيّر الأصل، وإمتى تستخدم structuredClone بدل الـ spread، وكام تكلفة كل طريقة بالأرقام.

المشكلة باختصار

الـ spread operator بيوحي إنه بيعمل نسخة كاملة. ده غير صحيح. { ...obj } بينسخ المستوى الأول من الكائن فقط. أي object متداخل جوّاه بيفضل مشتركًا بين النسخة والأصل.

النتيجة: تعدّل حقلًا في النسخة، فتلاقي نفس الحقل اتغيّر في الأصل. الـ bug ده مابيكسرش الكود فورًا. بيظهر بعد فترة على شكل «بيانات بتتغيّر لوحدها»، وده من أصعب الأنواع في التتبّع، لأن السطر اللي سبب المشكلة بعيد عن السطر اللي ظهرت فيه.

شاشة لابتوب تعرض أسطر كود برمجية ملونة، تعبيرًا عن التعامل مع كائنات جافاسكريبت ونسخها في الذاكرة

المفهوم الأول: الفرق بين القيمة والمرجع

قبل أي كود، خد المثال ده.

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

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

ده بالظبط الفرق في JavaScript. القيم الأولية (الرقم، النص، boolean) بتتنسخ زي الصورة الضوئية: نسخة مستقلة. الكائنات (objects و arrays) بتتنسخ زي المفتاح: بتنسخ طريق الوصول، مش المحتوى.

التعريف الدقيق: المتغيّر اللي قيمته object مابيخزّنش الكائن نفسه. بيخزّن مرجعًا (reference)، يعني عنوان مكان الكائن في الذاكرة. لما تكتب const b = a وكان a كائنًا، انت بتنسخ العنوان فقط. فـ a و b بقوا بيشاوروا على نفس الكتلة في الذاكرة، وأي تعديل من خلال أي واحد فيهم بيظهر في التاني.

كيف يعيش الكائن في الذاكرة

ذاكرة البرنامج فيها منطقتان مهمتان. الأولى اسمها الـ stack: سريعة ومنظّمة، وبتخزّن القيم صغيرة الحجم والثابتة زي الأرقام والمراجع. التانية اسمها الـ heap: مساحة أكبر ومرنة، وفيها بتتخزّن الكائنات والمصفوفات.

لما تكتب const user = { name: "Ali" }، بيحصل حاجتان. الكائن نفسه بيتحط في الـ heap. والمتغيّر user في الـ stack بياخد مرجعًا يشاور على مكان الكائن في الـ heap.

دلوقتي لما تعمل { ...user }، JavaScript بتنشئ كائنًا جديدًا في الـ heap للمستوى الأول، وبتنسخ قيم الحقول. الحقل اللي قيمته أولية بتتنسخ فعليًا. الحقل اللي قيمته كائن بيتنسخ مرجعه فقط. يعني الكائن المتداخل ما اتعملّوش نسخة جديدة في الـ heap؛ النسخة والأصل بيشاوروا على نفس الكتلة. ده مصدر الـ bug كله.

لوحة دوائر إلكترونية مكبّرة تمثّل ذاكرة الجهاز التي تُخزَّن فيها كائنات جافاسكريبت وتُشارَك مراجعها بين النسخ

يعني إيه Shallow Copy بالظبط

تخيّل ملف فيه أوراق، وعايز نسخة منه بسرعة. صوّرت غلاف الملف وقائمة المحتويات، لكن جوّه كتبت قصاد كل ورقة «راجع الملف الأصلي» بدل ما تصوّرها. فالملف الجديد ليه غلاف مستقل، إنما أوراقه هي نفس أوراق الملف القديم. لو حد عدّل ورقة، الملفّان بيتأثروا.

ده بالظبط الـ Shallow Copy. التعريف الدقيق: النسخ السطحي بينشئ كائنًا جديدًا، وبينسخ حقول المستوى الأول فقط. الحقول الأولية بتتنسخ بقيمتها، والحقول الكائنية بتتنسخ بمرجعها. الأدوات اللي بتعمل كده في JavaScript: { ...obj } و Object.assign({}, obj) للكائنات، و arr.slice() و [ ...arr ] للمصفوفات. كلها سطحية.

في المقابل، النسخ العميق (Deep Copy) بيمشي على كل المستويات. بينشئ نسخة جديدة لكل كائن متداخل، لحد آخر ورقة في الشجرة. النتيجة شجرة منفصلة تمامًا، تعديل أي حقل فيها مابيلمسش الأصل.

الكود اللي بيوريك المشكلة

JavaScript
const original = {
  name: "Ali",
  address: { city: "Cairo" }
};

// نسخة سطحية بالـ spread
const copy = { ...original };

copy.name = "Sara";           // حقل أولي: يغيّر النسخة فقط
copy.address.city = "Giza";   // حقل كائني: يغيّر الأصل كمان

console.log(original.name);          // "Ali"   صحيح
console.log(original.address.city);  // "Giza"  تغيّر بدون قصد

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

خد سيناريو واقعي. عندك متجر إلكتروني، وحالة السلة محفوظة في object اسمه cart جوّاه items array. مطوّر بيكتب const draft = { ...cart } قبل ما يعدّل كمية منتج، وهو متأكد إنه شغّال على نسخة. لكن draft.items هو نفس الـ array بتاع cart. أول draft.items[0].qty = 5 بيعدّل السلة الأصلية على طول، فالمستخدم بيشوف الكمية اتغيّرت من غير ما يضغط «حفظ». في مراجعة كود لفريق، طلعت النسخ السطحية الناقصة دي سبب حوالي 3 من كل 10 bugs في طبقة الـ state.

الحل: ثلاث طرق للنسخ العميق

عندك ثلاث طرق عملية، ولكل واحدة سعر.

1) structuredClone — الافتراضي اليوم

دالة مبنية في اللغة، بتعمل نسخة عميقة كاملة.

JavaScript
const original = {
  name: "Ali",
  address: { city: "Cairo" },
  createdAt: new Date()
};

const copy = structuredClone(original);

copy.address.city = "Giza";

console.log(original.address.city);            // "Cairo"  الأصل سليم
console.log(copy.createdAt instanceof Date);   // true     النوع محفوظ

structuredClone بتحافظ على أنواع زي Date و Map و Set و ArrayBuffer، وبتتعامل صح مع المراجع الدائرية (object بيشاور على نفسه). الافتراض هنا: إنك على Node.js 17 أو أحدث، أو متصفح حديث (مدعومة من منتصف 2022). على بيئة أقدم من كده، الدالة دي مش موجودة.

2) طريقة JSON — أبسط حل متوافق

JavaScript
const copy = JSON.parse(JSON.stringify(original));

سطر واحد، وشغّال في كل بيئة. لكن له حدود حقيقية: بيحذف الحقول اللي قيمتها undefined أو function، وبيحوّل Date لنص، وبيفقد Map و Set، وبيرمي خطأ لو في مرجع دائري. استخدمه فقط لو الكائن بيانات صرفة (نصوص وأرقام و booleans و arrays عادية).

3) lodash cloneDeep — للحالات المعقّدة

JavaScript
import cloneDeep from "lodash/cloneDeep";

const copy = cloneDeep(original);

بيتعامل مع كل شيء تقريبًا، بما فيه المراجع الدائرية والـ prototypes. التكلفة: مكتبة خارجية، وأبطأ من الاتنين التانيين.

الأرقام

قياس تقريبي على Node.js 22، لمصفوفة فيها 10,000 كائن، كل كائن جوّاه كائن address متداخل:

JavaScript
const data = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: "user" + i,
  address: { city: "Cairo", zip: "11511" }
}));

console.time("json");
JSON.parse(JSON.stringify(data));
console.timeEnd("json");             // ~14ms

console.time("structuredClone");
structuredClone(data);
console.timeEnd("structuredClone");  // ~9ms

في القياس ده: structuredClone حوالي 9 مللي ثانية، طريقة JSON حوالي 14 مللي ثانية، و lodash cloneDeep حوالي 21 مللي ثانية. الأرقام تقريبية وبتختلف حسب شكل البيانات وعمق التداخل، بس الترتيب ثابت غالبًا: structuredClone الأسرع، و JSON في الوسط، و cloneDeep الأبطأ مقابل مرونته الأكبر.

الـ trade-offs اللي لازم تعرفها

كل طريقة ليها سعر صريح. راجع النقاط دي قبل ما تختار.

  • structuredClone: بتكسب سرعة وأنواعًا محفوظة ودعم المراجع الدائرية. بتخسر: بترمي DataCloneError لو في function أو عنصر DOM جوّه الكائن، وبتفقد الـ prototype (نسخة من class بتطلع object عادي).
  • طريقة JSON: بتكسب البساطة والتوافق الكامل. بتخسر: undefined والدوال بتتحذف، وDate بتبقى نصًّا، وMap وSet بتضيع، والمرجع الدائري بيكسرها.
  • lodash cloneDeep: بتكسب تغطية شبه كاملة لكل الأنواع. بتخسر: حجم مكتبة إضافي وأداء أبطأ.
  • التكلفة العامة للنسخ العميق: بينسخ الشجرة كلها. على كائن كبير أو جوّه loop بيتكرر كتير، ده بيتحوّل لحمل ملموس على الـ CPU والذاكرة.

متى لا تستخدم النسخ العميق

النسخ العميق مش الحل الافتراضي لكل نسخة. تجنّبه في الحالات دي:

  • الكائن مسطّح: لو كل حقوله قيم أولية (مفيش تداخل)، الـ shallow copy كافية، وأسرع، وأرخص. النسخ العميق هنا شغل زيادة بدون فايدة.
  • بتعدّل المستوى الأول فقط: لو كل اللي محتاجه تغيير حقل سطحي واحد، { ...obj, name: "Sara" } بتكفي وبتسيب المتداخل زي ما هو بأمان.
  • تحديث state في React: الطريقة الصحيحة إنك تنسخ المسار اللي اتغيّر فقط، مش تعمل deep copy للـ state كله في كل render. نسخ كامل في كل تحديث بيهدر أداء بدون داعي.

الخطوة التالية

افتح أقرب مكان في الكود بتعمل فيه { ...obj } أو Object.assign لكائن جوّاه تداخل، قبل ما تعدّله. اسأل سؤالًا واحدًا: هل الكائن المتداخل بيتعدّل بعد النسخ؟ لو الإجابة نعم، حوّل السطر لـ structuredClone واكتب اختبارًا صغيرًا يتأكد إن الأصل ما اتغيّرش. لو وقتها طلعلك DataCloneError، ده معناه إن في function جوّه الكائن — انقل للـ lodash/cloneDeep في الحالة دي تحديدًا.

المصادر

  • MDN Web Docs — توثيق الدالة structuredClone() ودعم المتصفحات: developer.mozilla.org/en-US/docs/Web/API/structuredClone
  • MDN Web Docs — تعريف Shallow copy: developer.mozilla.org/en-US/docs/Glossary/Shallow_copy
  • MDN Web Docs — تعريف Deep copy: developer.mozilla.org/en-US/docs/Glossary/Deep_copy
  • Node.js Documentation — توفّر structuredClone كدالة عامة منذ الإصدار 17: nodejs.org/api/globals.html
  • Lodash Documentation — دالة cloneDeep: lodash.com/docs
  • HTML Living Standard (WHATWG) — خوارزمية النسخ المهيكل (structured clone) وقيودها مثل DataCloneError.

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة