To nie takie proste
Dla osoby dopiero zaczynającej swoją przygodę z JavaScriptem temat pozornie może być banalny. „Na chłopski rozum” wystarczyłoby przypisać nowej zmiennej wartość starej zmiennej i powinno działać. Zatem spróbujmy:
const myObject = {
name: "John"
}
const newObject = myObject;
console.log(myObject);
console.log(newObject);
console.log(myObject === newObject);
Działa! Jednakże, gdyby kopiowanie obiektów było aż tak proste, to nie powstałby ten artykuł. Dokonajmy małej zmiany w naszym kodzie. Skopiuj powyższy kod i pod linią z zadeklarowaną zmienną newObject doklej linię znajdującą się poniżej:
myObject.name = "Steve";
Typ referencyjny
Jak już pewnie zauważyłeś, właściwość name została zmieniona zarówno w obiekcie pierwotnym jak i nowym obiekcie. Spowodowane jest to tym, że obiekty są referencyjnym typem zmiennej. Co to oznacza w praktyce? Jedną z cech odróżniających je od typów prostych jest sposób przechowywania pożądanej wartości. Referencyjne typy zmiennych przechowują jedynie referencję (adres) pozwalający na dostanie się do naszego obiektu. Nasze dwie zmienne, które stworzyliśmy są tak naprawdę referencjami do tego samego obiektu.
To jak kopiowac obiekty w JavaScripcie?
Metod na stworzenie kopii będącej nowym, niezależnym obiektem jest wiele. Przedstawię poniżej kilka z nich, wraz z krótkim komentarzem.
Metoda cloneDeep – Lodash
Biblioteka Lodash oferuje nam metodę cloneDeep, której zadaniem jest tworzenie deep copies, czyli zupełnie nowych, niezależnych obiektów. Jej użycie jest banalne, co przedstawia kod poniżej:
const obj = { 'name': 'Bob' };
const deep = _.cloneDeep(obj);
obj.name = "Steve";
console.log(obj);
console.log(deep);
Główną zaletą tego rozwiązania na pewno jest prostota oraz brak konieczności zrozumienia jak dokładnie działa funkcja, której użyliśmy. Jednakże powstaje konieczność dodania kolejnej biblioteki do naszego projektu.
JSON.parse oraz JSON.stringify
W tym sposobie wykorzystujemy dwie metody obiektu JSON: stringify oraz parse. Pierwsza z nich odpowiada za konwersję wartości przekazanych do niej tak, aby było możliwe użycie ich za pomocą formatu danych JSON. Druga z nich działa na odwrót. Przekazujemy do niej dane w formacie JSON, a w wyniku jej działania otrzymujemy, w naszym przypadku, obiekt:
const myObject = {
name: "John"
}
const newObject = JSON.parse(JSON.stringify(myObject));
myObject.name = "Steve";
console.log(myObject);
console.log(newObject);
Problemem, który powstaje podczas wykorzystywania tego sposobu jest brak możliwości przekazywania wartości, których nie można przechowywać w JSON’ach. Takimi wartościami są na przykład NaN, czy undefined.
Użycie Object.assign()
Kolejnym ze sposobów na kopiowanie obiektów jest użycie metody assign. Metoda ta przyjmuje dwa parametry. Pierwszym jest nasze miejsce, do którego będziemy kopiować. W naszym wypadku przekażemy pusty obiekt, ponieważ wynik działania tej funkcji przypisujemy do zmiennej. Równie dobrze moglibyśmy stworzyć wcześniej pusty obiekt i podać jego nazwę w pierwszym parametrze – efekt byłby identyczny. Drugim parametrem jest źródło danych, z którego będziemy czerpać podczas tworzenia naszej kopii obiektu. Zobaczmy jak to wygląda w kodzie:
const myObject = {
name: "John",
parents: {
mom: "Lucy",
dad: "David"
}
}
const newObject = Object.assign({}, myObject);
myObject.name = "Steve";
myObject.parents.mom = "Mary";
console.log(myObject);
console.log(newObject);
W przykładzie, który zamieściłem widać pewien problem pojawiający się przy wykorzystaniu tej metody. Jeśli w naszym obiekcie będziemy przechowywać typy referencyjne to do nowego obiektu zostaną skopiowane zostaną ich referencje, a nie wartości. W konsekwencji tego modyfikacja obiektu znajdującego się w obiekcie zmieni wartość również w naszej sklonowanej wersji. Osobiście skorzytałbym z tej metody tylko wtedy, gdy miałbym absolutną pewność, że w otrzymanym obiekcie znajdują się wyłącznie typy proste.
Użycie spread operator
Następnym rozwiązaniem zaprezentowanym w tym wpisie jest użycie udogodnienia, które pojawiło się wraz ze standardem ES6, a mianowicie spread operator. Rozwiązanie to da nam dokładnie taki sam efekt jak użycie Object.assign() – tu również występuje problem przepisywania referencji dla typów referencyjnych w obiekcie.
const myObject = {
name: "John",
parents: {
mom: "Lucy",
dad: "David"
}
}
const newObject = { ...myObject };
myObject.name = "Steve";
myObject.parents.mom = "Mary";
console.log(myObject);
console.log(newObject);
Wykorzystanie pętli for…in
Jest to chyba najprostszy ze sposobów zawartych w tym artykule. Do sklonowania obiektu może nam posłuzyć najzwyklejsza pętla for…in. Poniżej przedstawiam przykład:
const myObject = {
name: "John"
}
const newObject = {};
for (let key in myObject) {
newObject[key] = myObject[key];
}
myObject.name = "Steve";
console.log(myObject);
console.log(newObject);
Tu także występuje problem przepisywania referencji, tak jak we wcześniejszych metodach.
Kilka słów podsumowania
Jak widać nie ma rozwiązań idealnych. Ostatnią opcją jaką mogę zaproponować jest stworzenie własnej funkcji do klonowania obiektów. Niemniej jednak jeśli nie jest to konieczne, to uważam, że warto skorzystać z rozwiązań, które zostały przedstawione w tym wpisie. Jak zawsze zachęcam do pozostawienia komentarza oraz do zajrzenia do źródeł.
Źródła i materiały dodatkowe:
https://we-are.bookmyshow.com/understanding-deep-and-shallow-copy-in-javascript-13438bad941c
https://scotch.io/bar-talk/copying-objects-in-javascript
https://stackoverflow.com/questions/38416020/deep-copy-in-es6-using-the-spread-sign
https://flaviocopes.com/how-to-clone-javascript-object/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
https://youtu.be/H1NmJIv1A2Y
Stworzenie nowej referencji do obiektu to nie jest shallow copy. Shallow copy to zachowanie, jakie uzyskamy przy użyciu `Object.assign` czy operatora spread: typy proste są kopiowane, natomiast w przypadku obiektów tworzona jest nowa referencja.
Poprawione, dzięki za uzupełnienie! 😉
Problem niestety jest dość poważny – zwłaszcza jeśli używamy np. Vuexa w Vue – brak wykonania tzw „głębokiej kopii” powoduje często modyfikacje stanu aplikacji poza mutacjami. Przerabialiśmy chyba wszystkie opisane tu sposoby i generalnie skończyliśmy na napisaniu własnej funkcji, którą używamy w naszych projektach – bo lepiej mieć 1 dopracowaną funkcję niż bez sensu dołączać całą bibliotekę Lodash (którą swego czasu zresztą często używałem…).
Czemu dołączacie całą bibliotekę Lodash? Akurat w jej przypadku można importować poszczególne funkcje → https://www.npmjs.com/package/lodash.clonedeep
To oczywiście lekkie uproszczenie, w naszym przypadku wypracowaliśmy własną funkcję, która dosłownie w kilku linijkach robi dokładnie to co ma robić 🙂