استنساخ عميق للكائنات في جافا سكريبت

في أي لغة برمجة ، هناك أنواع من البيانات يصفها المبرمجون للموضوعات من أجل مواصلة العمل ومعالجتها ، إذا لزم الأمر. جافا سكريبت ليست استثناء ، فهي تحتوي على أنواع بيانات ( Number ، String ، Boolean ، Symbol ، إلخ) ومرجع ( Array ، Object ، Function ، Maps ، Sets ، إلخ) أنواع البيانات. تجدر الإشارة إلى أن أنواع البيانات البدائية غير قابلة للتغيير - لا يمكن تعديل قيمها ، ولا يمكن الكتابة عليها إلا بقيمة جديدة كاملة ، ولكن مع أنواع البيانات المرجعية ، يكون العكس هو الصحيح. على سبيل المثال ، قم بتعريف متغيرات النوع Number Object :

 let num = 5; let obj = { a: 5 }; 

لا يمكننا تعديل المتغير num ، يمكننا فقط إعادة كتابة قيمته ، لكن يمكننا تعديل المتغير obj:

 let num = 10; let obj = { a: 5, b: 6 }; 

كما ترون ، في الحالة الأولى ، قمنا بالكتابة فوق قيمة المتغير ، وفي الحالة الثانية قمنا بتوسيع الكائن. من هذا نستنتج أن أنواع البيانات البدائية لا يمكن توسيعها ، وأنواع البيانات المرجعية يمكننا القيام بذلك ، حتى مع معدل const .

يمكن تجميد الأخير ، على سبيل المثال ، باستخدام Object.freeze(obj) ، ولكن هذا الموضوع خارج نطاق المقالة (ارتباطات لـ Object.defineProperty الغريبة ، تحمي الكائن من التغييرات ).

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

 const arr = [0, 1, 2, 3, 4, 5]; console.log("Array: ", arr); // output: Array: [0, 1, 2, 3, 4, 5] 

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

 const arr = [0, 1, 2, 3, 4, 5]; console.log("Old array: ", arr); // "Old array: " [0, 1, 2, 3, 4, 5] const newArr = insertValToArr(arr, 15); console.log("New array: ", newArr); // output: "New array: " [0, 1, 2, 3, 4, 5, 15] console.log("Old array: ", arr); // output: "Old array: " [0, 1, 2, 3, 4, 5, 15] function insertValToArr(arr, val) { const newArr = arr; newArr.push(val); return newArr; } 

كما نرى من استنتاجات وحدة التحكم ، لم يتم تغيير المصفوفة الجديدة فحسب ، بل أيضًا المصفوفة القديمة. حدث هذا لأنه في دالة insertValToArr قمنا ببساطة بتعيين صفيف واحد إلى const newArr = arr ، وبالتالي أنشأنا رابطًا const newArr = arr موجودة ، وعندما حاولنا تعديل صفيف جديد ، فإنه يشير إلى منطقة الذاكرة الخاصة بالصفيف القديم ، وتغييره تقريبًا. ونظرًا لأن كلا الصفيفين يشيران إلى نفس منطقة الذاكرة ، فسيكون لهما نفس القيمة. دعنا نغير وظيفتنا بحيث لا يمكن تغيير الصفيف القديم:

 const arr = [0, 1, 2, 3, 4, 5]; const newArr = insertValToArr(arr, 15); console.log("New array: ", newArr); // output: "New array: " [0, 1, 2, 3, 4, 5, 15] console.log("Old array: ", arr); // output: "Old array: " [0, 1, 2, 3, 4, 5] function insertValToArr(arr, val) { const newArr = []; arr.forEach((value, ind) => { newArr[ind] = value}); newArr.push(val); return newArr; } 

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

دعونا نلقي نظرة على الطرق القياسية لنسخ الكائنات التي يوفرها JavaScript - يتم استخدام Object.assign () لنسخ قيم جميع الخصائص الخاصة به من كائن مصدر واحد أو أكثر إلى الكائن الهدف. بعد النسخ ، تقوم بإرجاع الكائن الهدف. النظر فيه:

 const obj = { a: 1 }; const newObj = Object.assign({}, obj); console.log(newObj); // output: { a: 1, b: 2 } console.log(obj); // output: { a: 1, b: 2 } 

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

الخطوة 1: قم بتعريف وتهيئة الكائن Z ، وقم أيضًا بإجراء إخراج وحدة التحكم للمقارنة قبل الاستنساخ وبعده:

 const Z = { a: 5, b: { g: 8, y: 9, t: { q: 48 } }, x: 47, l: { f: 85, p: { u: 89, m: 7 }, s: 71 }, r: { h: 9, a: 'test', s: 'test2' } }; console.log('Z object before cloning: ', Z); 

صورة

الخطوة 2: تعيين الكائن Z إلى كائن refToZ لإظهار الفرق بين التخصيص الطبيعي والاستنساخ العميق:

 const refToZ = Z; 

الخطوة 3: تعيين الكائن Z كائن Y باستخدام الدالة deepClone وإضافة خاصية جديدة إلى الكائن Y بعد ذلك ، اعرض هذين الكائنين في وحدة التحكم:

 const Y = deepClone(Z); function deepClone(obj) { const clObj = {}; for(const i in obj) { if (obj[i] instanceof Object) { clObj[i] = deepClone(obj[i]); continue; } clObj[i] = obj[i]; } return clObj; } Y.addnlProp = { fd: 45 }; console.log('Z object after cloning: ', Z); console.log('Y object: ', Y); 

صورة

صورة

في وحدة التحكم ، نرى بوضوح أن تغيير الكائن Y ، إضافة خاصية جديدة ، لا نقوم بتغيير الكائن Z ولن يكون لدى addnlProp خاصية addnlProp في addnlProp .

الخطوة 4: تغيير الخاصية x ، الموجودة في نصي الكائنات Z و Y وعرض كلا الكائنين مرة أخرى في وحدة التحكم:

 Yx = 76; console.log('Y object: ', Y); console.log('Z object: ', Z); 

صورة

صورة

عن طريق تغيير نفس الخاصية في الكائن Y ، لا نؤثر على الخاصية في النص Z

الخطوة 5: في الخطوة الأخيرة ، للمقارنة ، نقوم ببساطة بإضافة خاصية addToZ بالقيمة 100 إلى كائن refToZ وعرض الكائنات الثلاثة في وحدة التحكم:

 refToZ.addToZ = 100; console.log('refToZ object: ', refToZ); console.log('Z object: ', Z); console.log('Y object: ', Y); 

صورة

صورة

صورة

بتغيير كائن refToZ قمنا أيضًا بتغيير Z ، لكن Y لم يتأثر. من هذا نستنتج أن deepClone تخلق كائنًا جديدًا مستقلاً له خصائص وقيمها من كائن موجود (يمكن العثور على الكود لتطبيق وظيفة deepClone على CodePen ).

دعونا نتناول تنفيذ هذه الوظيفة. يجد الأخير أي تداخل للكائن ، حتى دون معرفة ذلك. كيف تفعل هذا؟ الشيء هو أننا في هذه الحالة نستخدم الخوارزمية المعروفة للرسوم البيانية - البحث المتعمق. الكائن عبارة عن رسم بياني له فرع واحد أو عدة فروع ، والذي بدوره يمكن أن يكون له فروعه ، إلخ. من أجل أن نجد كل شيء ، نحتاج إلى الدخول في كل فرع والانتقال إلى عمقه ، لذلك سنجد كل عقدة في الرسم البياني ونحصل على قيمه. يمكن تنفيذ البحث العميق بطريقتين: التكرار واستخدام حلقة. الثانية قد تتحول إلى أسرع ، حيث إنها لن تملأ مكدس المكالمة ، والتي بدورها تفعل العودية. في deepClone لوظيفة deepClone استخدمنا مجموعة من العودية مع حلقة. إذا كنت ترغب في قراءة الكتب حول الخوارزميات ، فإنني أنصحك ببدء تشغيل Aditya Bhargava "خوارزميات Grokayem" أو "Thomas Kormen" الخوارزميات: البناء والتحليل ".

للتلخيص ، استرجعنا أنواع البيانات في JavaScript وكيفية انتقالها إلى الوظائف. نظرنا إلى مثال بسيط للاستنساخ المستقل لمجموعة بسيطة أحادية البعد. لقد نظرنا في أحد تطبيقات اللغة القياسية لنسخ الكائنات ، ونتيجة لذلك ، كتبت وظيفة صغيرة (بحجم) لاستنساخ عميق للكائنات المعقدة. يمكن أن تجد وظيفة مماثلة تطبيقها على جانب الخادم (Node js) ، وهو الأمر الأكثر احتمالًا على العميل. آمل أن يكون هذا المقال مفيدًا لك. اراك مجددا

Source: https://habr.com/ru/post/ar480786/


All Articles