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

أصبح النظام الأساسي مزودًا الآن بـOrganizationClone() ، وهي دالة مدمجة للنسخ العميق.

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

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

  • 98
  • 98
  • 94
  • 15.4

المصدر

النُسخ العشوائية

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

من طرق إنشاء نسخة سطحية في 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} على خصائص (enumerable) myOriginal باستخدام عامل الانتشار. وتستخدم اسم الخاصية وقيمتها، وتعينها واحدًا تلو الآخر إلى كائن فارغ تم إنشاؤه حديثًا. وعلى هذا النحو، يكون الكائن الناتج متطابقًا في الشكل، ولكن بنسخة خاصة به من قائمة الخصائص والقيم. يتم نسخ القيم أيضًا، ولكن يتم التعامل مع ما يُعرف بالقيم الأولية بشكل مختلف عن طريق قيمة JavaScript بشكل مختلف عن القيم غير الأولية. للاقتباس MDN:

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

MDN : الإصدار الأولي

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

النُسخ التفصيلية

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

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

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

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

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

الاستنساخ الهيكلي

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

لقد تغير ذلك الآن! تم تعديل مواصفات 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 - ربما يكون ذلك بسبب استخدامك هياكل بيانات غير قابلة للتغيير أو أنك تريد التأكد من أن دالة ما يمكنها معالجة كائن دون التأثير على الأصل - فلن تكون بحاجة إلى الوصول إلى حلول بديلة أو مكتبات. أصبحت منظومة JavaScript المتكاملة الآن تضم structuredClone(). حسنًا.