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, ...).