JavaScript: Wie kopiert man ein Objekt tief (Deep Clone)?

Dafür gibt es inzwischen etwas Eingebautes — structuredClone. Die Frage wurde 2008 gestellt und bekam damals die passende Antwort; interessant ist heute, was die einzelnen Varianten still und leise mit deinen Daten machen.

Warum eine Kopie keine Kopie ist

const shallow = { ...src };
shallow.nested.a[0] = "CHANGED";
after editing the copy, the ORIGINAL says: CHANGED

Spread und Object.assign kopieren die oberste Ebene. Verschachtelte Objekte werden als Referenz kopiert, beide “Kopien” zeigen also auf dasselbe.

structuredClone

const c = structuredClone(src);
original after editing the clone: 1

structuredClone ist im Browser und in Node global verfügbar. Ein Aufruf, keine Abhängigkeit.

Was der JSON-Umweg zerstört

JSON round-trip:
  date     String     "2020-01-01T00:00:00.000Z"
  map      Object     {}
  set      Object     {}
  regexp   Object     {}
  typed    Object     {"0":1,"1":2,"2":3}
  undef    KEY GONE   undefined
  inf      object     null
  nan      object     null

structuredClone:
  date     Date       Wed Jan 01 2020 00:00:00 GMT+0000
  map      Map        [object Map]
  set      Set        [object Set]
  regexp   RegExp     /ab+c/gi
  bigint   BigInt     10
  typed    Uint8Array 1,2,3
  undef    undefined  undefined
  inf      Number     Infinity
  nan      Number     NaN

JSON.parse(JSON.stringify(x)) ist die Antwort, die weiterhin überall hineinkopiert wird, und das ist, was sie tut. Ein Date kommt als String zurück, der kein .getTime() mehr hat. Eine Map und ein Set kommen als {} zurück — der Inhalt ist schlicht weg, ohne Fehler. NaN und Infinity werden null, und ein Schlüssel mit undefined verschwindet ganz, "k" in obj antwortet also anders als vorher.

Nichts davon wirft. Zwei Dinge schon:

BigInt         : TypeError: Do not know how to serialize a BigInt
circular ref   : TypeError: Converting circular structure to JSON

structuredClone kommt mit dem zyklischen Fall zurecht und behält die Identität:

structuredClone: ok, and cc.self === cc is true

Was structuredClone wegwirft

a function  : DOMException: () => 1 could not be cloned.
a class     : x = 1 , constructor = Object , typeof foo.hello = undefined
a prototype : own = 2 , inherited 'proto' = undefined
a getter    : becomes a plain value: 42

Eine Funktion wirft, laut und deutlich, das ist in Ordnung. Die zweite Zeile ist die, die man sich merken sollte: Das Klonen einer Klassen-Instanz wirft nicht. Du bekommst ein einfaches Objekt mit den Feldern und ohne Methoden, und constructor.name ist "Object". Der Fehler zeigt sich später, an anderer Stelle, als “is not a function”.

Getter werden auf den Wert eingedampft, den sie beim Klonen geliefert haben, und die Prototypen-Kette wird nicht verfolgt.

Das “effizient” aus der Frage

JSON round-trip        7.87 ms
structuredClone        9.40 ms

10.000 kleine, einfache Objekte — der beste Fall für JSON, und er gewinnt. Das ist eine Datenform und keine allgemeine Rangfolge, aber es reicht, um die Vorstellung zu beerdigen, man nehme structuredClone wegen der Geschwindigkeit. Man nimmt es, weil es bei Daten korrekt ist, die der JSON-Umweg verstümmelt.

Wenn das Objekt wirklich nur JSON-förmige Daten enthält und du es in einer heißen Schleife kopierst, ist der alte Trick weiterhin vertretbar. Überall sonst sind die Verluste oben der Grund dagegen.

Wo es das nicht gibt

structuredClone braucht Node 17 oder einen Browser von 2022. Darunter bleiben lodash.cloneDeep oder der JSON-Umweg mit bewusst in Kauf genommenen Verlusten.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).