Deeplink in JavaScript mit StructuredClone

Die Plattform bietet 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 Deep Copy 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 Verweise behandelt. Das bedeutet, dass beim Kopieren eines Werts in Wirklichkeit nur ein Verweis auf dasselbe zugrunde liegende Objekt kopiert wird, was zu einem oberflächlichen Kopierverhalten führt.

Tiefkopien

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. Dies kann sehr wichtig sein, um sicherzustellen, dass zwei Codeteile nicht versehentlich ein Objekt teilen und den Status des jeweils anderen unbeabsichtigt manipulieren.

Früher gab es keine einfache oder schöne Möglichkeit, in JavaScript eine tiefe Kopie eines Werts zu erstellen. Viele Entwickler haben Drittanbieterbibliotheken wie die cloneDeep()-Funktion von Lodash verwendet. Die wohl gängigste 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. Es ist zwar schnell, hat aber auch ein paar Mängel und Stolperdraht:

  • 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

Die Plattform benötigte bereits an mehreren Stellen die Möglichkeit, Deep Copies von JavaScript-Werten zu erstellen: Das Speichern eines JS-Werts in IndexedDB erfordert eine gewisse Form der Serialization, damit er auf dem Laufwerk 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 nun 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, unterstützt viele integrierte Datentypen und ist im Allgemeinen robuster und oft schneller.

Es gibt jedoch einige Einschränkungen, die Sie vielleicht überrascht werden:

  • 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. Dies führt dazu, dass structuredClone() ausgegeben wird.

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. In Anbetracht der Tatsache, dass die neue structuredClone() ohne den Aufwand durch den Missbrauch anderer APIs entfällt und robuster als JSON.parse() ist, empfehlen wir, sie als Standardmethode zum Erstellen von Deep-Kopien festzulegen.

Fazit

Wenn Sie in JS einen Deep-Copy eines Werts erstellen möchten, weil Sie beispielsweise 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!