Unterscheiden zwischen tiefen und flachen Kopien in JavaScript

Neu ist immer besser!

Sie haben sich mit Sicherheit schon einmal mit Kopien in JavaScript befasst, auch wenn Sie es nicht wussten. Vielleicht haben Sie auch von dem Paradigma in der funktionalen Programmierung gehört, dass Sie keine vorhandenen Daten ändern sollten. Dazu müssen Sie wissen, wie Sie Werte sicher in JavaScript kopieren können. Heute schauen wir uns an, wie das geht und vermeiden dabei die Fallstricke!

Was ist eine Kopie?

Eine Kopie sieht genauso aus wie das alte, ist es aber nicht. Wenn Sie die Kopie ändern, erwarten Sie, dass das Original gleich bleibt, während sich die Kopie ändert.

Bei der Programmierung speichern wir Werte in Variablen. Das Erstellen einer Kopie bedeutet, dass Sie eine neue Variable mit denselben Werten initiieren. Es gibt jedoch eine große potenzielle Gefahr: Tiefes Kopieren vs. flaches Kopieren . Eine tiefe Kopie bedeutet, dass alle Werte der neuen Variablen kopiert und von der ursprünglichen Variablen getrennt werden. Eine flache Kopie bedeutet, dass bestimmte (Unter-) Werte noch mit der ursprünglichen Variablen verbunden sind.

Um das Kopieren wirklich zu verstehen, müssen Sie wissen, wie JavaScript Werte speichert.

Primitive Datentypen

Zu den primitiven Datentypen gehören:

  • Nummer - zB 1
  • String - zB 'Hello'
  • Boolean - z true
  • undefined
  • null

Wenn Sie diese Werte erstellen, sind sie eng mit der Variablen gekoppelt, der sie zugewiesen sind. Sie existieren nur einmal. Das heißt, Sie müssen sich nicht wirklich um das Kopieren primitiver Datentypen in JavaScript kümmern. Wenn Sie eine Kopie erstellen, handelt es sich um eine echte Kopie. Sehen wir uns ein Beispiel an:

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

Durch Ausführen erstellen b = aSie die Kopie. Wenn Sie nun einen neuen Wert neu zuweisen b, bändert sich der Wert von , jedoch nicht von a.

Zusammengesetzte Datentypen - Objekte und Arrays

Technisch gesehen sind Arrays auch Objekte, daher verhalten sie sich gleich. Ich werde beide später im Detail durchgehen.

Hier wird es interessanter. Diese Werte werden beim Instanziieren tatsächlich nur einmal gespeichert, und durch Zuweisen einer Variablen wird nur ein Zeiger (eine Referenz) auf diesen Wert erstellt .

Wenn wir nun eine Kopie b = aerstellen und einen verschachtelten Wert in ändern b, ändert sich auch ader verschachtelte Wert, da aund btatsächlich auf dasselbe verweisen. Beispiel:

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

Im obigen Beispiel haben wir tatsächlich eine flache Kopie erstellt . Dies ist oftmals problematisch, da wir erwarten, dass die alte Variable die ursprünglichen Werte hat, nicht die geänderten. Wenn wir darauf zugreifen, erhalten wir manchmal eine Fehlermeldung. Es kann vorkommen, dass Sie versuchen, es eine Weile zu debuggen, bevor Sie den Fehler finden, da viele Entwickler das Konzept nicht wirklich verstehen und nicht erwarten, dass dies der Fehler ist.

Schauen wir uns an, wie wir Kopien von Objekten und Arrays erstellen können.

Objekte

Es gibt mehrere Möglichkeiten, Kopien von Objekten zu erstellen, insbesondere mit der neuen erweiterten und verbesserten JavaScript-Spezifikation.

Spread-Operator

Dieser mit ES2015 eingeführte Operator ist einfach großartig, weil er so kurz und einfach ist. Es "verteilt" alle Werte auf ein neues Objekt. Sie können es wie folgt verwenden:

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

Sie können damit beispielsweise auch zwei Objekte zusammenführen const c = {...a, ...b}.

Object.assign

Dies wurde meistens verwendet, bevor der Spread-Operator da war, und es macht im Grunde das Gleiche. Sie müssen jedoch vorsichtig sein, da das erste Argument in der Object.assign()Methode tatsächlich geändert und zurückgegeben wird. Stellen Sie also sicher, dass Sie das zu kopierende Objekt mindestens als zweites Argument übergeben. Normalerweise übergeben Sie nur ein leeres Objekt als erstes Argument, um zu verhindern, dass vorhandene Daten geändert werden.

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

Fallstricke: Verschachtelte Objekte

Wie bereits erwähnt, gibt es beim Kopieren von Objekten eine große Einschränkung, die für beide oben aufgeführten Methoden gilt. Wenn Sie ein verschachteltes Objekt (oder Array) haben und es kopieren, werden verschachtelte Objekte in diesem Objekt nicht kopiert, da es sich nur um Zeiger / Referenzen handelt. Wenn Sie das verschachtelte Objekt ändern, ändern Sie es daher für beide Instanzen, was bedeutet, dass Sie am Ende erneut eine flache Kopie erstellen würden . Beispiel: // SCHLECHTES BEISPIEL

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

Um eine tiefe Kopie von verschachtelten Objekten zu erstellen , müssten Sie dies berücksichtigen. Eine Möglichkeit, dies zu verhindern, besteht darin, alle verschachtelten Objekte manuell zu kopieren:

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

Wenn Sie sich gefragt haben, was zu tun ist, wenn das Objekt mehr Schlüssel als nur hat foods, können Sie das volle Potenzial des Spread-Operators nutzen. Wenn Sie nach dem weitere Eigenschaften übergeben ...spread, überschreiben sie beispielsweise die ursprünglichen Werte const b = {...a, foods: {...a.foods}}.

Tiefe Kopien machen, ohne nachzudenken

Was ist, wenn Sie nicht wissen, wie tief die verschachtelten Strukturen sind? Es kann sehr mühsam sein, große Objekte manuell zu durchsuchen und jedes verschachtelte Objekt von Hand zu kopieren. Es gibt eine Möglichkeit, alles zu kopieren, ohne nachzudenken. Sie einfach stringifyIhr Objekt und parsees gleich danach:

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

Hier müssen Sie berücksichtigen, dass Sie keine benutzerdefinierten Klasseninstanzen kopieren können. Sie können sie daher nur verwenden, wenn Sie Objekte mit nativen JavaScript-Werten kopieren .

Arrays

Das Kopieren von Arrays ist genauso üblich wie das Kopieren von Objekten. Ein Großteil der dahinter stehenden Logik ist ähnlich, da Arrays auch nur Objekte unter der Haube sind.

Spread-Operator

Wie bei Objekten können Sie den Spread-Operator verwenden, um ein Array zu kopieren:

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

Array-Funktionen - abbilden, filtern, reduzieren

Diese Methoden geben ein neues Array mit allen (oder einigen) Werten des ursprünglichen zurück. Währenddessen können Sie auch die Werte ändern, was sehr praktisch ist:

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

Alternativ können Sie das gewünschte Element beim Kopieren ändern:

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

Diese Methode wird normalerweise verwendet, um eine Teilmenge der Elemente zurückzugeben, die an einem bestimmten Index beginnt und optional an einem bestimmten Index des ursprünglichen Arrays endet. Wenn Sie array.slice()oder verwenden, erhalten array.slice(0)Sie eine Kopie des ursprünglichen Arrays.

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

Verschachtelte Arrays

Ähnlich wie bei Objekten wird bei Verwendung der oben beschriebenen Methoden zum Kopieren eines Arrays mit einem anderen Array oder Objekt darin eine flache Kopie generiert . Um dies zu verhindern, verwenden Sie auch JSON.parse(JSON.stringify(someArray)).

BONUS: Kopieren einer Instanz von benutzerdefinierten Klassen

Wenn Sie bereits ein Profi in JavaScript sind und sich mit Ihren benutzerdefinierten Konstruktorfunktionen oder -klassen befassen, möchten Sie möglicherweise auch Instanzen davon kopieren.

Wie bereits erwähnt, können Sie diese nicht einfach stringifizieren + analysieren, da Sie sonst Ihre Klassenmethoden verlieren. Stattdessen möchten Sie eine benutzerdefinierte copyMethode hinzufügen , um eine neue Instanz mit allen alten Werten zu erstellen. Mal sehen, wie das funktioniert:

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

Um mit Objekten und Arrays umgehen zu können, auf die in Ihrer Instanz verwiesen wird, müssen Sie Ihre neu erlernten Fähigkeiten zum Tiefenkopieren anwenden ! Ich werde nur eine endgültige Lösung für die benutzerdefinierte Konstruktormethode hinzufügen copy, um sie dynamischer zu gestalten:

Mit dieser Kopiermethode können Sie so viele Werte in Ihren Konstruktor einfügen, wie Sie möchten, ohne alles manuell kopieren zu müssen!

Über den Autor: Lukas Gisder-Dubé war Mitbegründer und Leiter eines Startups als CTO für 1 1/2 Jahre, um das Tech-Team und die Architektur aufzubauen. Nachdem er das Startup verlassen hatte, unterrichtete er Coding als Lead Instructor bei Ironhack und baut derzeit eine Startup Agency & Consultancy in Berlin auf. Schauen Sie sich dube.io an, um mehr zu erfahren.