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

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

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

دعم المتصفح

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

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

MDN: أساسي

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

النُسخ المكرّرة

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

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

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

وفي الواقع، كان هذا حلاً بديلاً شائعًا، أدى إلى تحسين الإصدار 8 من "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()، ننصحك بأن يكون النهج التلقائي لإنشاء نُسخ لصفحات في التطبيق.

الخاتمة

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