النسخ العميق في 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، العنصر الأساسي (القيمة الأساسية، نوع البيانات الأساسي) هو بيانات ليست كائنًا ولا تحتوي على طرق. هناك سبعة أنواع أساسية من البيانات: سلسلة وعدد وعدد كبير جدًا وقيمة منطقية وقيمة غير محدّدة ورمز وقيمة فارغة.

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 استخدام طريقة ما لتسلسل البيانات حتى يمكن تخزينها على القرص واستعادتها لاحقًا. وبالمثل، يتطلّب إرسال الرسائل إلى 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(). مبروك.