كيفية التمييز بين النسخ العميقة والضحلة في JavaScript

الجديد هو الأفضل دائمًا!

من المؤكد أنك تعاملت مع النسخ في JavaScript من قبل ، حتى لو لم تكن تعرف ذلك. ربما تكون قد سمعت أيضًا عن النموذج في البرمجة الوظيفية وهو أنه لا يجب عليك تعديل أي بيانات موجودة. للقيام بذلك ، يجب أن تعرف كيفية نسخ القيم بأمان في JavaScript. اليوم ، سننظر في كيفية القيام بذلك مع تجنب المزالق!

بادئ ذي بدء ، ما هي النسخة؟

تبدو النسخة مثل الشيء القديم ، لكنها ليست كذلك. عندما تقوم بتغيير النسخة ، تتوقع أن يظل الشيء الأصلي كما هو ، بينما تتغير النسخة.

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

لفهم النسخ حقًا ، عليك أن تدخل في كيفية تخزين JavaScript للقيم.

أنواع البيانات البدائية

تشمل أنواع البيانات البدائية ما يلي:

  • رقم - على سبيل المثال 1
  • سلسلة - على سبيل المثال 'Hello'
  • منطقية - على سبيل المثال true
  • undefined
  • null

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

const a = 5
let b = a // this is the copy
b = 6
console.log(b) // 6
console.log(a) // 5

بالتنفيذ b = a، تقوم بعمل النسخة. الآن ، عندما تعيد تعيين قيمة جديدة إلى b، فإن قيمة bالتغييرات ، ولكن ليس من a.

أنواع البيانات المركبة - كائنات وصفائف

من الناحية الفنية ، المصفوفات هي أيضًا كائنات ، لذا فهي تتصرف بنفس الطريقة. سوف أتناول كلاهما بالتفصيل لاحقًا.

هنا يصبح الأمر أكثر إثارة للاهتمام. يتم تخزين هذه القيم في الواقع مرة واحدة فقط عند إنشاء مثيل لها ، وتعيين متغير يؤدي فقط إلى إنشاء مؤشر (مرجع) لتلك القيمة .

الآن، إذا جعلنا نسخة b = a، وتغيير بعض القيمة المتداخلة في b، فإنه في الواقع تغيير aالصورة قيمة المتداخلة أيضا، منذ aو bتشير في الواقع إلى نفس الشيء. مثال:

const a = {
 en: 'Hello',
 de: 'Hallo',
 es: 'Hola',
 pt: 'Olà'
}
let b = a
b.pt = 'Oi'
console.log(b.pt) // Oi
console.log(a.pt) // Oi

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

دعنا نلقي نظرة على كيفية عمل نسخ من الكائنات والمصفوفات.

شاء

هناك طرق متعددة لعمل نسخ من الكائنات ، خاصة مع توسيع وتحسين مواصفات JavaScript الجديدة.

عامل الانتشار

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

const a = {
 en: 'Bye',
 de: 'Tschüss'
}
let b = {...a}
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss

يمكنك أيضًا استخدامه لدمج كائنين معًا ، على سبيل المثال const c = {...a, ...b}.

كائن. تعيين

تم استخدام هذا في الغالب قبل أن يكون عامل الانتشار موجودًا ، وهو يقوم بنفس الشيء بشكل أساسي. يجب أن تكون حذرًا ، لأن الوسيطة الأولى في Object.assign()الطريقة يتم تعديلها وإعادتها بالفعل. لذا تأكد من تمرير الكائن لنسخه على الأقل باعتباره الوسيطة الثانية. عادةً ما تقوم فقط بتمرير كائن فارغ باعتباره الوسيطة الأولى لمنع تعديل أي بيانات موجودة.

const a = {
 en: 'Bye',
 de: 'Tschüss'
}
let b = Object.assign({}, a)
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss

المأزق: الكائنات المتداخلة

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

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = {...a}
b.foods.dinner = 'Soup' // changes for both objects
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Soup

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

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = {foods: {...a.foods}}
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

في حال كنت تتساءل عما يجب فعله عندما يحتوي الكائن على مفاتيح أكثر من فقط foods، يمكنك استخدام الإمكانات الكاملة لعامل الانتشار. عند تمرير المزيد من الخصائص بعد ...spread، فإنها تقوم بالكتابة فوق القيم الأصلية ، على سبيل المثال const b = {...a, foods: {...a.foods}}.

عمل نسخ عميقة دون تفكير

ماذا لو كنت لا تعرف مدى عمق الهياكل المتداخلة؟ قد يكون من الممل للغاية المرور يدويًا عبر الكائنات الكبيرة ونسخ كل كائن متداخل يدويًا. هناك طريقة لنسخ كل شيء دون تفكير. أنت ببساطة stringifyكائنك parseوبعده مباشرة:

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = JSON.parse(JSON.stringify(a))
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

هنا ، عليك أن تضع في اعتبارك أنك لن تكون قادرًا على نسخ مثيلات فئة مخصصة ، لذلك يمكنك استخدامها فقط عند نسخ كائنات بقيم JavaScript أصلية بداخلها.

المصفوفات

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

عامل الانتشار

كما هو الحال مع الكائنات ، يمكنك استخدام عامل الانتشار لنسخ مصفوفة:

const a = [1,2,3]
let b = [...a]
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

وظائف المصفوفة - الخريطة والتصفية والتقليل

ستعيد هذه التوابع مصفوفة جديدة بكل (أو بعض) قيم الأصل. أثناء القيام بذلك ، يمكنك أيضًا تعديل القيم ، وهو أمر مفيد جدًا:

const a = [1,2,3]
let b = a.map(el => el)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

بدلاً من ذلك ، يمكنك تغيير العنصر المطلوب أثناء النسخ:

const a = [1,2,3]
const b = a.map((el, index) => index === 1 ? 4 : el)
console.log(b[1]) // 4
console.log(a[1]) // 2

صفيف. شريحة

تُستخدم هذه الطريقة عادةً لإرجاع مجموعة فرعية من العناصر ، بدءًا من فهرس معين وتنتهي اختياريًا عند فهرس محدد للمصفوفة الأصلية. عند استخدام array.slice()أو array.slice(0)سينتهي بك الأمر بنسخة من المصفوفة الأصلية.

const a = [1,2,3]
let b = a.slice(0)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

المصفوفات المتداخلة

على غرار الكائنات ، فإن استخدام الطرق المذكورة أعلاه لنسخ مصفوفة مع مصفوفة أخرى أو كائن بداخلها سيولد نسخة ضحلة . لمنع ذلك ، استخدم أيضًا JSON.parse(JSON.stringify(someArray)).

BONUS: نسخ نسخة من الفئات المخصصة

عندما تكون بالفعل محترفًا في JavaScript وتتعامل مع وظائف أو فئات المُنشئ المخصصة ، فربما ترغب في نسخ مثيلات هذه أيضًا.

كما ذكرنا من قبل ، لا يمكنك فقط تشديدها + تحليلها ، حيث ستفقد طرق الفصل الدراسي. بدلاً من ذلك ، قد ترغب في إضافة copyطريقة مخصصة لإنشاء مثيل جديد بكل القيم القديمة. دعونا نرى كيف يعمل ذلك:

class Counter {
 constructor() {
 this.count = 5
 }
 copy() {
 const copy = new Counter()
 copy.count = this.count
 return copy
 }
}
const originalCounter = new Counter()
const copiedCounter = originalCounter.copy()
console.log(originalCounter.count) // 5
console.log(copiedCounter.count) // 5
copiedCounter.count = 7
console.log(originalCounter.count) // 5
console.log(copiedCounter.count) // 7

للتعامل مع الكائنات والمصفوفات المشار إليها داخل المثال الخاص بك ، سيكون عليك تطبيق مهاراتك المكتسبة حديثًا حول النسخ العميق ! سأضيف حلًا نهائيًا copyلطريقة المُنشئ المخصص لجعلها أكثر ديناميكية:

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

نبذة عن الكاتب: شارك Lukas Gisder-Dubé في تأسيس وقيادة شركة ناشئة كمدير فني للتكنولوجيا لمدة عام ونصف ، حيث قام ببناء فريق التكنولوجيا والهندسة المعمارية. بعد ترك الشركة الناشئة ، قام بتدريس الترميز كمدرب رئيسي في Ironhack ويقوم الآن ببناء وكالة Startup Agency & Consultancy في برلين. تحقق من dube.io لمعرفة المزيد.