النسخ العميق في JavaScript باستخدام OrganizationClone

تتضمّن المنصة الآن دالة structuredClone()‎، وهي دالة مضمّنة للنسخ العميق.

لفترة طويلة، كان عليك اللجوء إلى الحلول البديلة والمكتبات لإنشاء نسخة مفصّلة من قيمة JavaScript. تتضمّن المنصة الآن structuredClone()، وهي دالة مدمجة للنسخ العميق.

توافق المتصفّح

  • Chrome: 98.
  • ‫Edge: 98
  • ‫Firefox: 94
  • ‫Safari: 15.4

المصدر

النُسخ غير الكاملة

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

إحدى طرق إنشاء نسخة سطحية في JavaScript باستخدام عامل انتشار الكائن ...:

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

إنّ إضافة موقع أو تغييره مباشرةً في النسخة السطحية لن يؤثّر إلا في النسخة، وليس في النسخة الأصلية:

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

ومع ذلك، فإنّ إضافة سمة مدمجة بشكل كبير أو تغييرها تؤثّر في كل من النسخة والنسخة الأصلية:

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

يكرّر التعبير {...myOriginal} السمات (القابلة للعدّ) لـ myOriginal باستخدام مشغّل التوسيع. ويستخدم اسم السمة وقيمتها، ويحدّدهما واحدًا تلو الآخر لكائن فارغ تم إنشاؤه حديثًا. وهكذا، يكون الكائن الناتج متطابقًا من حيث الشكل، ولكن مع نسخة خاصة به من قائمة السمات والقيم. تتم أيضًا نسخ القيم، ولكن تتعامل قيمة JavaScript مع القيم الأوّلية على نحو مختلف عن القيم غير الأوّلية. لعرض اقتباس من رقم دليل الجوّال:

في جافا سكريبت، القيمة الأولية (القيمة الأولية، نوع البيانات الأولية) هي بيانات ليست كائنًا وليس لها طرق. هناك سبعة أنواع أساسية من البيانات: سلسلة وعدد وعدد كبير جدًا وقيمة منطقية وقيمة غير محدّدة ورمز وقيمة فارغة.

MDN - عنصر أساسي

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

النُسخ المفصّلة

يُطلق على النسخة التي تتضمّن معلومات أكثر من نسخة سطحية اسم نسخة شاملة. تنسخ خوارزمية النسخ العميق أيضًا سمات العنصر الواحد تلو الآخر، ولكنها تستدعي نفسها بشكل متكرّر عندما تعثر على إشارة إلى عنصر آخر، ما يؤدي إلى إنشاء نسخة من هذا العنصر أيضًا. يمكن أن يكون ذلك مهمًا جدًا للتأكّد من أنّ رمزَين لا يشتركان عن طريق الخطأ في عنصر ويتلاعبان بحالة بعضهما البعض بدون قصد.

لم تكن هناك طريقة سهلة أو مناسبة لإنشاء نسخة طبق الأصل من قيمة في JavaScript. اعتمد الكثير من الأشخاص على مكتبات تابعة لجهات خارجية، مثل دالة cloneDeep() في Lodash. كان من بين الحلول الأكثر شيوعًا لهذه المشكلة عملية اختراق مستندة إلى JSON:

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

في الواقع، كان هذا الحلّ البديل شائعًا جدًا، لذا حسّنت V8 هذا الإجراء بشكل كبير JSON.parse()، ولا سيما النمط أعلاه، لجعله أسرع ما يمكن. وعلى الرغم من أنّ هذا التطبيق سريع، فإنّه يتضمّن بعض أوجه القصور والعوائق:

  • هياكل البيانات المتكرّرة: سيُرسِل JSON.stringify() خطأ عند تقديم هيكل بيانات متكرّر. ويمكن أن يحدث ذلك بسهولة تامة عند العمل مع القوائم أو الأشجار المرتبطة.
  • الأنواع المضمّنة: سيتم طرح JSON.stringify() إذا كانت القيمة تحتوي على أنواع أخرى مضمّنة في JavaScript مثل Map أو Set أو Date أو RegExp أو ArrayBuffer.
  • الدوالّ: سيتجاهل JSON.stringify() الدوالّ بدون إشعار.

الاستنساخ المنظم

كان النظام الأساسي بحاجة إلى إمكانية إنشاء نُسخ طبق الأصل من قيم JavaScript في مكانَين: يتطلّب تخزين قيمة JavaScript في IndexedDB استخدام شكل من أشكال التسلسل حتى يمكن تخزينها على القرص وإعادة تحويلها لاحقًا إلى شكلها الأصلي لاستعادة قيمة JavaScript. وبالمثل، يتطلّب إرسال الرسائل إلى WebWorker عبر postMessage() نقل قيمة JS من نطاق JS إلى نطاق آخر. يُطلق على الخوارزمية المستخدمة في ذلك اسم "النسخ الاحتياطي المنظم"، ولم يكن من السهل على المطورين الوصول إليها حتى وقت قريب.

وقد تغير ذلك الآن! تم تعديل مواصفات HTML لعرض دالة تُسمى structuredClone() تعمل على تنفيذ هذه الخوارزمية بالضبط كوسيلة للمطوّرين لإنشاء نُسخ دقيقة من قيم JavaScript بسهولة.

const myDeepCopy = structuredClone(myOriginal);

هذا كل شيء. هذه هي واجهة برمجة التطبيقات بالكامل. إذا كنت تريد التعمّق أكثر في التفاصيل، اطّلِع على مقالة MDN.

الميزات والقيود

يعالج الاستنساخ منظَّم العديد من أوجه القصور في تقنية JSON.stringify() (ولكن ليس كلها). يمكن أن يتعامل التكرار المنظَّم مع هياكل البيانات الدورية، ويتوافق مع العديد من أنواع البيانات المضمّنة، وهو بشكل عام أكثر ثباتًا وأسرع في كثير من الأحيان.

ومع ذلك، لا تزال هذه الطريقة تفرض بعض القيود التي قد تفاجئك:

  • النماذج الأولية: في حال استخدام structuredClone() مع مثيل فئة، ستحصل على عنصر عادي كقيمة السلسلة المرجعية المعروضة، لأنّ الاستنساخ المنظَّم يتخلّص من سلسلة النماذج الأولية للعنصر.
  • الدوال: إذا كان الكائن يحتوي على دوال، ستطرح structuredClone() استثناء DataCloneError.
  • العناصر غير القابلة للاستنساخ: بعض القيم غير قابلة للاستنساخ، وأبرزها Error وعقد DOM. سيؤدي ذلك إلى رمي structuredClone().

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

الأداء

لم أُجرِ مقارنة جديدة لمقاييس الأداء الدقيقة، إلا أنّ أجريت مقارنة في أوائل عام 2018 قبل ظهور structuredClone(). في ذلك الوقت، كان JSON.parse() الخيار الأسرع للكائنات الصغيرة جدًا. أتوقع أن يبقى الأمر على ما هو عليه. كانت الأساليب التي اعتمدت على الاستنساخ المنظم أسرع (إلى حد كبير) بالنسبة للأشياء الأكبر حجمًا. بما أنّ الإصدار structuredClone() الجديد لا يحتاج إلى إساءة استخدام واجهات برمجة التطبيقات الأخرى، كما أنّه أكثر فعالية من JSON.parse()، ننصحك باعتماده هو الطريقة التلقائية لإنشاء نُسخ لصفحات في التطبيق.

الخاتمة

إذا كنت بحاجة إلى إنشاء نسخة طبق الأصل من قيمة في JavaScript، ربما لأنّك تستخدم هياكل بيانات غير قابلة للتغيير أو تريد التأكّد من أنّ الدالة يمكنها التلاعب بعنصر بدون التأثير في العنصر الأصلي، لن تحتاج بعد الآن إلى استخدام الحلول البديلة أو المكتبات. يتضمّن الآن structuredClone(). مبروك.