Kopírování/klonování objektů v JS
Publikováno: 4.1.2021
Jak správně a úspěšně zkopírovat objekt v JavaScriptu. Popíšeme různé metody a rozdíly mezi nimi.
Text vyšel původně na webu autora.
V JS dochází při kopírování objektů k trochu odlišné situaci než při kopírování obsahu proměnných.
Je-li cílem zkopírovat obsah proměnné prvni
do proměnné druhy
, jde to provést následovně (živá ukázka):
var prvni = 'hodnota'
var druhy = prvni
druhy = 'jinaHodnota'
console.log(prvni) // hodnota
console.log(druhy) // jinaHodnota
Stejným způsob způsobem se může nabízet zkopírovat/naklonovat objekt.
var prvni = {
vlastnost: 'hodnota'
}
var druhy = prvni
console.log(prvni) // {"vlastnost":"hodnota"}
console.log(druhy) // {"vlastnost":"hodnota"}
Na první pohled to vypadá funkčně. Problém ale nastává, když se změní nějaká vlastnost zkopírovaného objektu (živá ukázka):
var prvni = {
vlastnost: 'hodnota'
}
var druhy = prvni
druhy.vlastnost = 'jinaHodnota'
console.log(prvni) // {"vlastnost":"jinaHodnota"}
console.log(druhy) // {"vlastnost":"jinaHodnota"}
Jak je vidět z výstupu JS konsole, oba objekty jsou stejné. Proč? Tímto způsobem se nekopíruje objekt, ale jen se na něj vytváří reference/odkaz. Nepochopení tohoto principu vede ke značným problémům.
...Spread
operátor
Řešení je použít tzv. spread operátor – tři tečky bezprostředně před názvem proměnné/objektu (živá ukázka):
var prvni = {
vlastnost: 'hodnota'
}
var druhy = { ...prvni }
druhy.vlastnost = 'jinaHodnota'
console.log(prvni) // {"vlastnost":"hodnota"}
console.log(druhy) // {"vlastnost":"jinaHodnota"}
Totéž jde zapsat i zkráceně jako:
var prvni = {
vlastnost: 'hodnota'
}
var druhy = { ...prvni, ...{ vlastnost: 'jinaHodnota' } }
Tedy při přiřazení rovnou změnit nějakou vlastnost kopírovaného objektu.
Spread operátor (v překladu z angličtiny něco jako rozložit/rozprostřít) byl standardizován až v roce 2018 v ECMAScriptu 2018 (zkráceně označovaném jako ES2018 nebo ES9). Nefunguje tedy ve starších prohlížečích.
Nejen z tohoto důvodu je občas možné vidět věci jako:
var druhy = JSON.parse(JSON.stringify(prvni))
Funguje to trochu jinak než třítečkový operátor, ale v tomto případě to účel plní stejně. Živá ukázka
Vzhledem k tomu, že to nejprve převádí objekt na řetězec a následně parsuje zpět na objekt, není to výkonově úplně nejlepší. Spíš nouzové řešení.
Další způsob je použít Object.assign
. Ten byl standardizován dříve než ...spread
operátor, takže v případě psaní JS kódu, který se už nekompiluje nástrojem typu Babel, může dávat větší smysl používat toto řešení (živá ukázka):
var druhy = Object.assign({}, prvni)
Hluboké a mělké klonování
Při klonování objektů je třeba rozlišovat tzv. hluboké klonování (deep clone) a mělké (shallow clone).
Spread operátor ...
dělá právě to mělké.
To se projeví tak, že změna hodnoty o úroveň níž (v příkladu dalsiVlastnost
) se projeví i u klonovaného objektu (živá ukázka):
var prvni = {
vlastnost: 'hodnota',
dalsiVlastnost: {
text: 'ahoj'
}
}
var druhy = { ...prvni }
druhy.vlastnost = 'jinaHodnota'
druhy.dalsiVlastnost.text = 'fytopuf'
console.log(prvni) // {"vlastnost":"hodnota","dalsiVlastnost":{"text":"fytopuf"}}
console.log(druhy) // {"vlastnost":"jinaHodnota","dalsiVlastnost":{"text":"fytopuf"}}
Tedy první úroveň je naklonovaná, ale hlouběji už je jen reference na původní objekt.
Mělkou kopii vytváří i konstrukce Object.assign
.
Co s tím?
Asi nejjednodušší řešení bez používání cizích knihoven je již výše zmíněný JSON.parse(JSON.stringify(objekt))
(živá ukázka).
Není to ale z výše popsaných důvodů úplně čisté řešení. Další problém je v tom, že se hodí jen pro klonování primitivních datových typů jako je řetězec (String), číslo (Number) nebo true/false (Boolean).
Pokud bude v objektu třeba funkce, tento převod tam a zase zpět nepřežije.
Knihovny pro deep copy
Jako best-practice považuji použít např. funkci cloneDeep
z populární knihovny Lodash (živá ukázka):
var prvni = {
vlastnost: 'hodnota',
dalsiVlastnost: {
text: 'ahoj'
}
}
var druhy = _.cloneDeepWith(prvni)
druhy.vlastnost = 'jinaHodnota'
druhy.dalsiVlastnost.text = 'fytopuf'
console.log(prvni) // {"vlastnost":"hodnota","dalsiVlastnost":{"text":"ahoj"}}
console.log(druhy) // {"vlastnost":"jinaHodnota","dalsiVlastnost":{"text":"fytopuf"}}
I kdysi populární knihovna jQuery má funkci extend
, která umí hloubkově klonovat objekty (živá ukázka):
var druhy = $.extend(true, {}, prvni);