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