Deeplink in JavaScript mit StructuredClone

Die Plattform wird jetzt mit „structuredClone()“ ausgeliefert, einer integrierten Funktion für das Deep-Copy-Verfahren.

Am längsten mussten Sie auf Behelfslösungen und Bibliotheken zurückgreifen, um eine tiefe Kopie eines JavaScript-Werts zu erstellen. Die Plattform wird jetzt mit structuredClone() ausgeliefert, einer integrierten Funktion für das Kopieren im Detail.

Unterstützte Browser

  • Chrome: 98. <ph type="x-smartling-placeholder">
  • Edge: 98. <ph type="x-smartling-placeholder">
  • Firefox: 94 <ph type="x-smartling-placeholder">
  • Safari: 15.4 <ph type="x-smartling-placeholder">

Quelle

Flache Kopien

Das Kopieren von Werten in JavaScript ist fast immer oberflächlich, 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, eine oberflächliche Kopie in JavaScript mit dem Objektverteilungsoperator ... zu erstellen:

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

const myShallowCopy = {...myOriginal};

Wenn Sie eine Eigenschaft direkt in der oberflächlichen 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 stark verschachtelten Eigenschaft 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 mit dem Spread Operator über die (auflistbaren) Eigenschaften von myOriginal. Sie verwendet den Namen und den Wert der Eigenschaft und weist sie nacheinander einem neu erstellten, leeren Objekt zu. Daher ist das resultierende Objekt von der Form her identisch mit einer eigenen Kopie der Liste der Eigenschaften und Werte. Die Werte werden ebenfalls kopiert, aber sogenannte Primitive werden vom JavaScript-Wert anders behandelt als nicht primitive Werte. So zitieren Sie MDN:

In JavaScript sind Daten, die kein Objekt sind und keine Methoden haben, als primitiver Wert (Primitiver Wert, primitiver Datentyp). Es gibt sieben primitive Datentypen: Zeichenfolge, Zahl, Bigint, boolescher Wert, undefiniert, Symbol und Null.

MDN – Primitive

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 flachen Kopie ist eine tiefe Kopie. Ein Deep-Copy-Algorithmus kopiert auch die Eigenschaften eines Objekts einzeln, ruft sich jedoch rekursiv auf, wenn er einen Verweis auf ein anderes Objekt findet. Dabei wird auch eine Kopie dieses Objekts erstellt. Dies kann sehr wichtig sein, um sicherzustellen, dass zwei Codeelemente nicht versehentlich ein Objekt gemeinsam nutzen und sich nicht unwissentlich gegenseitig verändern.

Früher gab es keine einfache oder schöne Möglichkeit, in JavaScript eine tiefe Kopie eines Werts zu erstellen. Viele Nutzer haben sich auf Bibliotheken von Drittanbietern wie die cloneDeep()-Funktion von Lodash verlassen. Die wahrscheinlich häufigste Lösung für dieses Problem war ein JSON-basierter Hack:

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

Da diese Problemumgehung so beliebt war, wurde JSON.parse() von V8 aggressiv optimiert, insbesondere das Muster oben, um die Geschwindigkeit so schnell wie möglich zu erhöhen. Es ist zwar schnell, hat aber auch ein paar Mängel und Stolperdraht:

  • Rekursive Datenstrukturen: JSON.stringify() wird ausgelöst, wenn Sie ihm eine rekursive Datenstruktur geben. Dies kann ganz leicht passieren, wenn Sie mit verknüpften Listen oder Baumstrukturen arbeiten.
  • Integrierte Typen: JSON.stringify() wird ausgelöst, wenn der Wert andere integrierte JS-Elemente wie Map, Set, Date, RegExp oder ArrayBuffer enthält.
  • Funktionen: Mit JSON.stringify() werden Funktionen unbemerkt verworfen.

Strukturiertes Klonen

Die Plattform benötigte bereits an verschiedenen Stellen die Möglichkeit, tiefe Kopien von JavaScript-Werten zu erstellen: Für das Speichern eines JS-Werts in IndexedDB ist eine Art Serialisierung erforderlich, damit er auf der Festplatte gespeichert und später deserialisiert werden kann, um den JS-Wert wiederherzustellen. Ebenso muss zum 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 heißt „Structured Klon“ und war bis vor Kurzem für Entwickler nicht leicht zugänglich.

Das hat sich nun geändert. Die HTML-Spezifikation wurde geändert, um eine Funktion namens structuredClone() bereitzustellen, die genau diesen Algorithmus ausführt, damit Entwickler auf einfache Weise tiefe Kopien von JavaScript-Werten erstellen können.

const myDeepCopy = structuredClone(myOriginal);

Geschafft! Das ist die gesamte API. Weitere Informationen findest du im MDN-Artikel.

Funktionen und Einschränkungen

Das strukturierte Klonen behebt viele (wenn auch nicht alle) Mängel der JSON.stringify()-Technik. 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. enthalten, da beim strukturierten Klonen die Prototypkette des Objekts verworfen wird.
  • Funktionen: Wenn Ihr Objekt Funktionen enthält, löst structuredClone() die Ausnahme DataCloneError aus.
  • Nicht klonbare Werte: Einige Werte, insbesondere Error- und DOM-Knoten, können nicht strukturiert werden. Es führt dazu, dass structuredClone() wirft.

Wenn eine dieser Einschränkungen für Ihren Anwendungsfall ein Problem darstellt, bieten Bibliotheken wie Lodash dennoch benutzerdefinierte Implementierungen anderer Deep-Cloning-Algorithmen, die für Ihren Anwendungsfall möglicherweise oder nicht geeignet sind.

Leistung

Ich habe zwar keinen neuen Mikro-Benchmark-Vergleich angestellt, aber ich habe Anfang 2018 einen Vergleich durchgeführt, bevor structuredClone() angezeigt wurde. Damals war JSON.parse() die schnellste Option für sehr kleine Objekte. Ich erwarte, dass sich das nicht ändern wird. Techniken zum strukturierten Klonen waren bei größeren Objekten (deutlich) 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, empfehle ich Ihnen, sie als Standardmethode zum Erstellen von Deep-Kopien festzulegen.

Fazit

Wenn Sie eine tiefe Kopie eines Werts in JS erstellen müssen, z. B. weil Sie unveränderliche Datenstrukturen verwenden oder eine Funktion ein Objekt bearbeiten möchten, ohne das Original zu beeinträchtigen, müssen Sie nicht mehr nach Behelfslösungen oder Bibliotheken suchen. Das JavaScript-Ökosystem verfügt jetzt über structuredClone(). Huzzah.