كما تعلمون في JavaScript ، يتم نسخ الكائنات حسب المرجع. لكن في بعض الأحيان تحتاج إلى القيام باستنساخ عميق لكائن ما. تقدم العديد من مكتبات js تنفيذها للدالة deepClone لهذه الحالة. لكن لسوء الحظ ، لا تأخذ معظم المكتبات في الاعتبار العديد من الأشياء المهمة:
- قد تكمن المصفوفات في الكائن ومن الأفضل نسخها كصفائف
- قد يحتوي الكائن على حقول برمز كمفتاح
- تحتوي حقول الكائنات على واصفات غير الافتراضية
- يمكن أن تكون الوظائف في حقول الكائن كما يجب أيضًا استنساخها.
- يحتوي الكائن أخيرًا على نموذج أولي مختلف عن Object.prototype
الذي كسرها ، وضعت الرمز الكامل تحت المفسدfunction deepClone(source) { return ({ 'object': cloneObject, 'function': cloneFunction }[typeof source] || clonePrimitive)(source)(); } function cloneObject(source) { return (Array.isArray(source) ? () => source.map(deepClone) : clonePrototype(source, cloneFields(source, simpleFunctor({}))) ); } function cloneFunction(source) { return cloneFields(source, simpleFunctor(function() { return source.apply(this, arguments); })); } function clonePrimitive(source) { return () => source; } function simpleFunctor(value) { return mapper => mapper ? simpleFunctor(mapper(value)) : value; } function makeCloneFieldReducer(source) { return (destinationFunctor, field) => { const descriptor = Object.getOwnPropertyDescriptor(source, field); return destinationFunctor(destination => Object.defineProperty(destination, field, 'value' in descriptor ? { ...descriptor, value: deepClone(descriptor.value) } : descriptor)); }; } function cloneFields(source, destinationFunctor) { return (Object.getOwnPropertyNames(source) .concat(Object.getOwnPropertySymbols(source)) .reduce(makeCloneFieldReducer(source), destinationFunctor) ); } function clonePrototype(source, destinationFunctor) { return destinationFunctor(destination => Object.setPrototypeOf(destination, Object.getPrototypeOf(source))); }
تنفيذ عملي مكتوب بأسلوب وظيفي يوفر لي الموثوقية والاستقرار والبساطة. ولكن بما أن العديد من المؤسف لا يزالون ، لسوء الحظ ، لا يستطيعون إعادة بناء تفكيرهم بالإجرائية والخطأ الزائف ، فسأشرح كل لبنة من تنفيذي:
سوف تأخذ وظيفة deepClone نفسها مصدر وسيطة واحد - المصدر الذي سنستنسخ منه ، وسيتم إرجاع نسختها العميقة مع جميع الميزات المذكورة أعلاه:
function deepClone(source) { return ({ 'object': cloneObject, 'function': cloneFunction }[typeof source] || clonePrimitive)(source)(); }
كل شيء بسيط هنا ، اعتمادًا على نوع البيانات في المصدر ، يتم تحديد وظيفة يمكنها استنساخها ، ويتم نقل المصدر نفسه إليها.
يمكنك أيضًا ملاحظة أن النتيجة التي يتم إرجاعها تسمى وظيفة بدون معاملات قبل إعادتها إلى المستخدم. هذا أمر ضروري ، لأنني ألغت القيمة التي استنسختها ، في أبسط المشغلات ، حتى أكون قادرًا على تحورها دون انتهاك نقاء الوظائف الإضافية. هنا هو تنفيذ هذا functor:
function simpleFunctor(value) { return mapper => mapper ? simpleFunctor(mapper(value)) : value; }
يمكنه القيام بأمرين - خريطة (إذا تم تمرير وظيفة المخطط إليه) واستخراج (إذا لم يتم تمرير أي شيء).
الآن سنقوم بتحليل الوظائف الإضافية cloneObject و cloneFunction و clonePrimitive. يأخذ كل واحد منهم حجة مصدر من نوع معين وإرجاع استنساخه.
يجب أن يراعي تطبيق
cloneObject أن الصفائف هي أيضًا من نوع كائن ، وكذلك ، في حالات أخرى ، يجب أن تقوم باستنساخ الحقول والنموذج الأولي. هنا هو تنفيذها:
function cloneObject(source) { return (Array.isArray(source) ? () => source.map(deepClone) : clonePrototype(source, cloneFields(source, simpleFunctor({}))) ); }
يمكن نسخ المصفوفة باستخدام طريقة الشريحة ، ولكن نظرًا لأن لدينا استنساخًا عميقًا ولا يمكن أن تحتوي المصفوفة على قيم بدائية فقط ، يتم استخدام طريقة الخريطة مع deepClone الموضح أعلاه كوسيطة.
بالنسبة للكائنات الأخرى ، نقوم بإنشاء كائن جديد ولفه في برنامجنا الموضح أعلاه ، واستنساخ الحقول (جنبًا إلى جنب مع الواصفات) باستخدام وظيفة مساعد cloneFields ، ثم استنساخ النموذج الأولي باستخدام clonePrototype.
وظائف المساعد سوف أصف أدناه. في غضون ذلك ، النظر في تنفيذ
cloneFunction :
function cloneFunction(source) { return cloneFields(source, simpleFunctor(function() { return source.apply(this, arguments); })); }
لا يمكنك ببساطة استنساخ وظيفة بكل المنطق. ولكن يمكنك لفه في وظيفة أخرى تستدعي الأصل بجميع الوسائط والسياق ، وترجع النتيجة. مثل هذا "الاستنساخ" سيبقي بالتأكيد الوظيفة الأصلية في الذاكرة ، لكنه "يزن" قليلاً ويعيد إنتاج المنطق الأصلي بالكامل. نحن نلتف الوظيفة المستنسخة في برنامج functor وباستخدام cloneFields ، نقوم بنسخ جميع الحقول من الوظيفة الأصلية إليها ، لأن الوظيفة في JS هي أيضًا كائن ، يسمى فقط ، وبالتالي يمكن تخزين الحقول فيه.
من المحتمل أن يكون للوظيفة نموذج أولي مختلف عن Function.prototype ، لكنني لم أعتبر هذه الحالة القصوى. واحد من سحر FP هو أنه يمكننا بسهولة إضافة غلاف جديد على وظيفة موجودة من أجل تنفيذ الوظائف اللازمة.
يخدم لبنة البناء clonePrimitive الأخيرة لاستنساخ القيم البدائية. ولكن بما أن القيم البدائية يتم نسخها حسب القيمة (أو بالرجوع إليها ، ولكنها غير قابلة للتغيير في بعض تطبيقات محركات JS) ، يمكننا ببساطة نسخها. ولكن نظرًا لأنه ليس من المتوقع أن نحصل على قيمة نقية ، ولكن يمكن لف القيمة التي يتم لفها في مُخرج والتي يمكن استخراجها بدون وسيطات ، أن نلتزم بقيمتنا في دالة:
function clonePrimitive(source) { return () => source; }
الآن نقوم بتنفيذ الوظائف الإضافية التي تم استخدامها أعلاه - clonePrototype و cloneFields
لاستنساخ نموذج أولي ، سيقوم
clonePrototype ببساطة باستخلاص النموذج الأولي من الكائن المصدر ، ومن خلال إجراء عملية خريطة على المُخرج الناتج ، قم بتعيينه على الكائن الهدف:
function clonePrototype(source, destinationFunctor) { return destinationFunctor(destination => Object.setPrototypeOf(destination, Object.getPrototypeOf(source))); }
تعد حقول الاستنساخ أكثر تعقيدًا ، لذا
أقسم وظيفة
cloneFields إلى قسمين. تأخذ الوظيفة الخارجية سلسلة من جميع الحقول المسماة وجميع حقول الرموز ، وتستقبل جميع الحقول تمامًا ، وتديرها خلال المخفض الذي أنشأته الوظيفة الإضافية:
function cloneFields(source, destinationFunctor) { return (Object.getOwnPropertyNames(source) .concat(Object.getOwnPropertySymbols(source)) .reduce(makeCloneFieldReducer(source), destinationFunctor) ); }
يجب على makeCloneFieldReducer إنشاء وظيفة مخفض بالنسبة لنا والتي يمكن تمريرها إلى طريقة الاختزال في صفيف من جميع حقول الكائن المصدر. كبطارية ، سيتم استخدام functor لدينا الذي يخزن الهدف. يجب على المخفض استخراج المقبض من حقل الكائن المصدر وتعيينه إلى حقل الكائن الهدف. ولكن من المهم هنا مراعاة أن هناك نوعين من الواصفات - مع القيمة و مع get / set. من الواضح أن هناك حاجة إلى استنساخ القيمة ، لكن مع get / set لا توجد مثل هذه الحاجة ، يمكن إرجاع مثل هذا الوصف كما هو:
function makeCloneFieldReducer(source) { return (destinationFunctor, field) => { const descriptor = Object.getOwnPropertyDescriptor(source, field); return destinationFunctor(destination => Object.defineProperty(destination, field, 'value' in descriptor ? { ...descriptor, value: deepClone(descriptor.value) } : descriptor)); }; }
هذا كل شيء. مثل هذا التطبيق ل deepClone يحل جميع المشاكل التي طرحت في بداية المقال. بالإضافة إلى ذلك ، فهي مبنية على وظائف نقية ووظيفة واحدة ، والتي توفر جميع الضمانات المتأصلة في حساب التفاضل والتكامل لامدا.
كما أنني لاحظت أنني لم أطبق سلوكًا ممتازًا لمجموعات أخرى غير صفيف يستحق الاستنساخ بشكل فردي ، مثل Map أو Set. رغم أنه في بعض الحالات ، قد يكون هذا ضروريًا.