Deeplink in JavaScript mit StructuredClone

Die Plattform enthält jetzt die integrierte Funktion „structuredClone()“, mit der eine Deep-Copy erstellt werden kann.

Lange Zeit mussten Sie auf Umwege und Bibliotheken zurückgreifen, um eine Deepcopy eines JavaScript-Werts zu erstellen. Die Plattform enthält jetzt structuredClone(), eine integrierte Funktion für das Deep-Copy.

Unterstützte Browser

  • Chrome: 98.
  • Edge: 98.
  • Firefox: 94.
  • Safari: 15.4.

Quelle

Shallow copies

Das Kopieren eines Werts in JavaScript ist fast immer flach, im Gegensatz zu tief. Das bedeutet, dass Änderungen an tief verschachtelten Werten sowohl in der Kopie als auch im Original sichtbar sind.

Eine Möglichkeit, in JavaScript eine flache Kopie mit dem Objekt-Ausbreitungsoperator ... zu erstellen:

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

Wenn Sie ein Attribut direkt in der flachen Kopie hinzufügen oder ändern, wirkt sich das nur auf die Kopie aus, nicht auf das Original:

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

Das Hinzufügen oder Ändern einer tief verschachtelten Property wirkt sich jedoch sowohl auf die Kopie als auch auf das Original aus:

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

Der Ausdruck {...myOriginal} iteriert mithilfe des Spreizoperators über die (zählbaren) Properties von myOriginal. Dabei werden der Name und der Wert der Property einem neu erstellten, leeren Objekt zugewiesen. Das resultierende Objekt hat also dieselbe Form, aber eine eigene Kopie der Liste der Eigenschaften und Werte. Auch die Werte werden kopiert, aber sogenannte primitive Werte werden vom JavaScript-Wert anders behandelt als nicht primitive Werte. Zitat aus der MDN:

In JavaScript sind primitive Daten (primitiver Wert, primitiver Datentyp) Daten, die kein Objekt sind und keine Methoden haben. Es gibt sieben primitive Datentypen: String, Zahl, BigInt, Boolescher Wert, Undefiniert, Symbol und Null.

MDN – Primitiv

Nicht primitive Werte werden als Referenzen behandelt. Das bedeutet, dass beim Kopieren des Werts nur eine Referenz auf dasselbe zugrunde liegende Objekt kopiert wird, was zu einem flachen Kopiervorgang führt.

Deep Copies

Das Gegenteil einer oberflächlichen Kopie ist eine tiefe Kopie. Ein Deep-Copy-Algorithmus kopiert die Eigenschaften eines Objekts ebenfalls einzeln, ruft sich aber rekursiv auf, wenn er einen Verweis auf ein anderes Objekt findet, und erstellt auch eine Kopie dieses Objekts. Das kann sehr wichtig sein, damit zwei Code-Snippets nicht versehentlich ein Objekt gemeinsam nutzen und den Status des jeweils anderen unbeabsichtigt manipulieren.

Bisher gab es keine einfache oder elegante Möglichkeit, eine Deepcopy eines Werts in JavaScript zu erstellen. Viele Entwickler haben Drittanbieterbibliotheken wie die cloneDeep()-Funktion von Lodash verwendet. Die wohl häufigste Lösung für dieses Problem war ein JSON-basierter Hack:

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

Diese Lösung war so beliebt, dass V8 JSON.parse() und insbesondere das oben beschriebene Muster aggressiv optimiert hat, um die Ausführung so schnell wie möglich zu machen. Und obwohl es schnell ist, hat es einige Nachteile und Stolpersteine:

  • Rekursiv strukturierte Daten: JSON.stringify() gibt eine Fehlermeldung aus, wenn Sie eine rekursive Datenstruktur angeben. Das kann bei der Arbeit mit verknüpften Listen oder Bäumen ganz leicht passieren.
  • Vordefinierte Typen: JSON.stringify() wird ausgegeben, wenn der Wert andere vordefinierte JS-Typen wie Map, Set, Date, RegExp oder ArrayBuffer enthält.
  • Funktionen: JSON.stringify() verwirft Funktionen geräuschlos.

Strukturiertes Klonen

Auf der Plattform war es bereits an mehreren Stellen erforderlich, Deep Copies von JavaScript-Werten zu erstellen: Das Speichern eines JS-Werts in IndexedDB erfordert eine gewisse Form der Serialization, damit er auf der Festplatte gespeichert und später deserialisiert werden kann, um den JS-Wert wiederherzustellen. Ebenso muss beim Senden von Nachrichten an einen WebWorker über postMessage() ein JS-Wert von einem JS-Bereich in einen anderen übertragen werden. Der dafür verwendete Algorithmus wird als „strukturierter Klon“ bezeichnet und war bis vor Kurzem für Entwickler nur schwer zugänglich.

Das hat sich jetzt geändert. Die HTML-Spezifikation wurde um eine Funktion namens structuredClone() ergänzt, die genau diesen Algorithmus ausführt. So können Entwickler ganz einfach Deep Copies von JavaScript-Werten erstellen.

const myDeepCopy = structuredClone(myOriginal);

Geschafft! Das ist die gesamte API. Weitere Informationen finden Sie im MDN-Artikel.

Funktionen und Einschränkungen

Beim strukturierten Klonen werden viele (aber nicht alle) Mängel der JSON.stringify()-Methode behoben. Das strukturierte Klonen kann zyklische Datenstrukturen verarbeiten, viele vordefinierte Datentypen unterstützen und ist im Allgemeinen robuster und oft schneller.

Es gibt jedoch einige Einschränkungen, die Sie überraschen könnten:

  • Prototypen: Wenn Sie structuredClone() mit einer Klasseninstanz verwenden, erhalten Sie ein einfaches Objekt als Rückgabewert, da beim strukturierten Klonen die Prototypkette des Objekts verworfen wird.
  • Funktionen: Wenn Ihr Objekt Funktionen enthält, löst structuredClone() eine DataCloneError-Ausnahme aus.
  • Nicht klonbare Elemente: Einige Werte können nicht strukturiert geklont werden, insbesondere Error- und DOM-Knoten. Dadurch wird structuredClone() ausgelöst.

Wenn eine dieser Einschränkungen für Ihren Anwendungsfall ein Dealbreaker ist, bieten Bibliotheken wie Lodash benutzerdefinierte Implementierungen anderer Deep-Cloning-Algorithmen, die möglicherweise zu Ihrem Anwendungsfall passen oder auch nicht.

Leistung

Ich habe zwar keinen neuen Mikro-Benchmark-Vergleich durchgeführt, aber einen Vergleich Anfang 2018, bevor structuredClone() veröffentlicht wurde. Damals war JSON.parse() die schnellste Option für sehr kleine Objekte. Das wird sich nicht ändern. Bei größeren Objekten waren Techniken, die auf strukturiertem Klonen basieren, (signifikant) schneller. Da die neue structuredClone() ohne den Overhead des Missbrauchs anderer APIs auskommt und robuster ist als JSON.parse(), empfehle ich, sie als Standardansatz für das Erstellen von Deep Copies zu verwenden.

Fazit

Wenn Sie in JS einen Deep-Copy eines Werts erstellen möchten, z. B. weil Sie unveränderliche Datenstrukturen verwenden oder sicherstellen möchten, dass eine Funktion ein Objekt bearbeiten kann, ohne das Original zu beeinflussen, müssen Sie nicht mehr auf Umwege oder Bibliotheken zurückgreifen. Das JS-System hat jetzt structuredClone(). Hurra!