Klonowanie obiektów w JavaScript - okładka

Kopiowanie obiektów w JavaScript

Opublikowano Kategorie Frontend, JavaScriptCzas czytania 7min

Dla programistów dopiero zaczynających swoją przygodę z JavaScriptem temat pozornie może być banalny. „Na chłopski rozum” wystarczy przypisać nowej zmiennej wartość starej zmiennej i powinno działać. W celu potwierdzenia postawionej tezy przygotowałem poniższy przykład.


const myObject = {
  name: 'John'
}

const newObject = myObject;

console.log(myObject);
console.log(newObject);
console.log(myObject === newObject); // true

Wartość ostatniego loga mogłaby sugerować, że operacja się powiodła. Gdyby kopiowanie obiektów było aż tak proste, to nie powstałby ten artykuł. Aby pokazać, gdzie leży problem konieczne będzie dokonanie małej zmiany w przygotowanych obiektach.


const myObject = {
  name: 'John'
}

const newObject = myObject;

console.log(myObject === newObject); // true
myObject.name = "Steve";

console.log(myObject);
console.log(newObject);

Okazuje się, że imię zostało zmienione zarówno w myObject, jak i w newObject. Sprawcą tego zamieszania jest referencyjny charakter typu object.

Typ referencyjny

Obiekty w JavaScript są referencyjnym typem zmiennej. Co to oznacza w praktyce? Cechą odróżniającą typy proste od typów referencyjnych jest sposób przechowywania pożądanej wartości. Referencyjne typy zmiennych przechowują jedynie referencję do miejsca w pamięci, gdzie przechowywana jest wartość zmiennej typu object. Typem referencyjnym w JavaScript są również tablice, które również są typem object. Możesz to potwierdzić, wywołując polecenie typeof [] w konsoli.

We wcześniejszym przykładzie, zmienne myObjectnewObject przechowują referencję do tego samego obiektu w pamięci. W konsekwencji modyfikacja dokonana z odwołaniem do jednej zmiennej jest również widoczna przy odwołaniu się do drugiej. Inną widoczną konsekwencją tego zjawiska jest pozorna możliwość modyfikacji obiektów przypisanych do zmiennych typu const. Pozorna, ponieważ zmieniając właściwości obiektu, jego referencja nie ulega zmianie. Zatem warunek stałości nie zostaje złamany.

Klonowanie obiektów w JavaScript - szturmowcy

Jak kopiować obiekty w JavaScript?

Wybór metody będzie determinował rodzaj kopii, jaką chcesz wykonać. Kopii możesz dokonać na trzech poziomach:

  • kopia na poziomie referencji. W tym przypadku zwykła operacja przypisania jest wystarczająca;
  • kopia typu shallow copy — skopiowany zostanie obiekt nadrzędny, lecz wszystkie wartości będące typami referencyjnymi będą miały wspólne referencje dla obiektu kopiowanego oraz kopii. Innymi słowy, zmiana w zagnieżdżonym obiekcie obiektu kopiowanego będzie widoczna w obiekcie skopiowanym;
  • kopia typu deep copy — kopiowane są również zagnieżdzone wartości będące typami referencyjnymi, dzięki czemu obiekt kopiowany oraz kopia są całkowicie rozłączne.

W zależności od potrzeb, do dyspozycji są różne metody.

Użycie Object.assign()

Jednym ze sposobów na kopiowanie obiektów na poziomie shallow copy jest użycie metody Object.assign(). Metoda ta przyjmuje dwa parametry. Pierwszym jest obiekt, do którego będziemy kopiować. Możesz przekazać pusty obiekt lub referencję do już istniejącego obiektu. Chcąc kopiować obiekt, najwięcej sensu ma przekazanie pustego obiektu i przypisanie rezultatu wywołania metody do nowej zmiennej. Drugim parametrem jest kopiowany obiekt lub referencja do niego.


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);

Użycie spread operator

Następnym rozwiązaniem pozwalającym na kopiowanie obiektów na poziomie shallow copy jest wykorzystanie spread operatora. Rozwiązanie daje dokładnie taki sam efekt jak użycie Object.assign().


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

Trzecim sposobem na przygotowanie shallow copy jest przeiterowanie po obiekcie z wykorzystaniem pętli for...in. Z tym podejściem należy jednak uważać. Pętla for...in iteruje jedynie po właściwościach, dla których property descriptor definiuje je jako enumerable. Aby mieć pewność, że wszystkie klucze zostaną skopiowane możesz wykorzystać wcześniej opisane metody lub przeiterować po kluczach kopiowanego obiektu wykorzystując pętlę for...of.


const myObject = {
  name: 'John'
}

const newObject = {};
const newObject2 = {};

for (const key in myObject) {
  newObject[key] = myObject[key];
}

for(const key of Object.keys(myObject)) {
  newObject2[key] = myObject[key];
}

myObject.name = 'Steve';
newObject2.name = 'Mark';


console.log(myObject, newObject, newObject2);

Metody JSON.parse oraz JSON.stringify

Ten sposób zakłada sprowadzenie obiektu do postaci stringa metodą JSON.stringify, a następnie przeparsowanie go do postaci nowego obiektu wykorzystując JSON.parse.


const myObject = {
  name: 'John'
}

const newObject = JSON.parse(JSON.stringify(myObject));
myObject.name = 'Steve';

console.log(myObject);
console.log(newObject);

Rezultatem takiego zabiegu jest uzyskanie deep copy. Problemem, który powstaje podczas wykorzystywania tego sposobu, jest brak możliwości przekazywania wartości, których nie można przechowywać w formacie JSON. Takimi wartościami są na przykład obiekty Date, NaN, czy undefined. Pamiętaj też, że przedstawione podejście mocno obciąża CPU i w przypadku sporych obiektów kopiowanie obiektu zajmie sporo czasu oraz będzie zasobożerne. Osobiście odradzam korzystanie z tego podejścia, gdyż może ono zauważalnie wpłynąć na wydajność Twojej aplikacji.

Metoda structuredClone

Jest to metoda, która w standardzie JavaScript pojawiła się całkiem niedawno. Globalna metoda structuredClone pozwala na tworzenie kopii na poziomie deep copy i korzysta z algorytmu klonowania strukturalnego.


const book = {
  title: 'Domain-Driven Design',
  languages: [
    {
      language: 'CN',
      available: 4
    },
    {
      language: 'PL',
      available: 1
    }
  ]
}

const bookCopy = structuredClone( book )

bookCopy.languages[ 0 ].language = 'US';

console.log( book, bookCopy );

Metoda cloneDeep z biblioteki lodash

Jest to dość archaiczne rozwiązanie. Mimo że lodash jest wciąż szalenie popularną biblioteką w środowisku JavaScript, to jej użycie w 2023 roku budzi wątpliwości. Część funkcji, które oferuje lodash, zostało zaimplementowane w standardzie języka JavaScript. Pozostałą część możesz zaimplementować samodzielnie, pisząc prostą funkcję. Prawdopodobnie jest to główną przyczyną tego, że ostatni commit w repozytorium lodasha został dodany w 2021 roku.

Jedną z metod, jakie lodash oferuje, jest metoda cloneDeep zwracająca kopię na poziomie deep clone. Wykorzystanie metody cloneDeep przedstawia poniższy snippet:


const obj = { 'name': 'Bob' };
 
const deep = _.cloneDeep(obj);
obj.name = 'Steve';
console.log(obj);
console.log(deep); 

W bibliotece lodash możesz też znaleźć analogiczną metodę clone tworzącą kopię na poziomie shallow copy.

Z uwagi na wspomniane już czynniki, uważam, że wykorzystanie lodasha do kopiowania obiektów nie ma sensu. Wyjątkiem są projekty, gdzie wykorzystywany jest JavaScript, który nie posiada wsparcia dla nowych rozwiązań lub nie może ich wykorzystać. Przykładem mogą być aplikacje korzystające z bardzo starej wersji Node.js lub aplikacje wymagające wsparcia dla starszych wersji przeglądarki Internet Explorer. W takim wypadku rekomenduję dodać zależność w postaci pojedynczej funkcji z pakietu lodash, zamiast dodawać całą bibliotekę do drzewa zależności.

Podsumowanie

Ostatnią opcją, jaką mogę Ci zaproponować, jest stworzenie własnej funkcji do kopiowania obiektów. Patrząc jednak na mnogość przedstawionych opcji, uważam, że nie ma to większego sensu. Mam nadzieję, że udało Ci się dowiedzieć czegoś ciekawego. Pytanie o kopiowanie obiektów jest jednym z pytań w moim e-booku 106 Pytań Rekrutacyjnych Junior JavaScript Developer. Sprawdź też koniecznie źródła i materiały dodatkowe, by dowiedzieć się o obiektach w JavaScript nieco więcej.

Źródła i materiały dodatkowe

*Artykuł jest odświeżoną wersją wpisu z 2018 roku. Chciałbym gorąco podziękować Radosławowi z bloga Front Cave, który pokazał mi metodę structuredClone oraz przyczynił się do powstania tego wpisu.

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Zapisując się na mój mailing, otrzymasz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Powiadom o
guest

5 komentarzy
oceniany
najnowszy najstarszy
Inline Feedbacks
View all comments
Maciej Walczak
Maciej Walczak
2 lat temu

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

Comandeer
2 lat temu
Reply to  Maciej Walczak

Czemu dołączacie całą bibliotekę Lodash? Akurat w jej przypadku można importować poszczególne funkcje → https://www.npmjs.com/package/lodash.clonedeep

Maciej Walczak
Maciej Walczak
2 lat temu
Reply to  Comandeer

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ć 🙂

Comandeer
5 lat temu

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.

Dominik Szczepaniak
5 lat temu
Reply to  Comandeer

Poprawione, dzięki za uzupełnienie! 😉