Kopiowanie to mechanizm tworzenia duplikatów. W przypadku prymitywów (string, number, boolean itd) sprawa jest prosta, możemy spokojnie wykonywać wszystkie operacje bez obaw o nadpisywanie oryginału.

Sprawa się komplikuje przy zastosowaniu przypisania obiektu za pomocą prostego = - wtedy tworzy się referencja do oryginału. Kopia jest zależna od oryginału, więc każda modyfikacja w obiekcie oddziałuje na resztę obiektów. Czasami taki efekt nie jest pożądany, niestety w js brakuje natywnych, prostych sposobów na klonowanie obiektów bez tworzenia referencji (wskaźnika na oryginał), trzeba w tym celu wykorzystywać różne hacki.

W tym artykule omówię część z nich, mam nadzieję że komuś się przydadzą.

Trochę teorii

Istnieją rodzaje kopiowania obiektów w js:

  • Brak - referencja, wskazuje na adres obiektu macierzystego. Przy podmianie jakiejkolwiek wartości obiektu, zamieniają się one równocześnie w kopii i oryginale. Kopia jest silnie zależna od oryginału.
  • Płytkie (shallow) - otrzymujemy prostą kopię, która posiada jednopoziomową kopię, głębsze zagnieżdzenia są referencją do oryginału i przy podmianie wartości zmieni się również w innych obiektach.
  • Głębokie (deep) - kopie są identyczne i niezależne od siebie. Obiekt jest autonomiczny, posiada wartości na własność, nic nie współdzieli - zmiana w oryginale nie zmodyfikuje kopii.

Brak kopii

Zacznijmy od tego, w jaki sposób nie skopiujemy obiektu, a który jednak początkowo wydaje się słusznym i prostym rozwiązaniem. Jak widać, przy podmianie wartości w klonie, wartość oryginału również się zmieniła, co nie jest oczekiwanym efektem.

// proste przypisanie  
var strawberry = {
    color: 'red',
    taste: 'sweet',
    size: 'large',
    shape: {
        x: 10,
        y: 32,
        z: 33
    }
};

var someOtherStrawberry = strawberry;
someOtherStrawberry.size = 'small'
console.log(strawberry.size); 
// output: small

Płytkie kopie

Część funkcji dostarczonych natywnie przez JavaScript umożliwia płytkie skopiowanie obiektu. Dopóki zajmujemy się przestrzenią bez zagnieżdżeń, wszystko działa jak powinno.

Problem pojawia się, gdy modyfikujemy zagnieżdżone obiekty - wtedy nadal możemy modyfikować oryginał, co nie jest pożądane (dzięki rekurencyjnemu użyciu płytkich kopii możemy jednak stworzyć głęboką kopię, czym zajmiemy się później).

Poniżej sposób na płytką kopię:

Object.assign()

//Tworzy płytką kopię - klonuje właściwości najwyższego poziomu, pozostawiając referencję do zagnieżdzonych  
var someOtherStrawberry = Object.assign({}, strawberry);  
someOtherStrawberry.size = 'small';  
console.log(strawberry.size) 
// output: large  

someOtherStrawberry.shape.x = 32  
console.log(strawberry.shape.x) 
// output: 32, wartość strawberry jest podmieniona przez modyfikacje na someOtherStrawberry!

Spread operator ES6

Standard ES6 przynosi nam proste ułatwienia, pozwalające na dokonywanie kopii. Jednym z nich jest spread operator ..., który płytko ignoruje wskaźnik do obiektu macierzystego. Dzięki temu otrzymujemy obiekty niezależne.

var strawberry = {
    color: 'red',
    taste: 'sweet',
    size: 'large',
    shape: {
        x: 10,
        y: 32,
        z: 33
    }
};
const someOtherStrawberry = {
    ...strawberry
};
someOtherStrawberry.size = 'small';
console.log(strawberry.size) 
// output: large, wszystko OK!  

someOtherStrawberry.shape.x = 32;
console.log(strawberry.shape.x) 
// output: 32, znowu podmienione przez modyfikacje na someOtherStrawberry :(

Sposoby na głęboką kopię

JSON.parse(), JSON.stringify()

Kiedy tworzymy klona, chcielibyśmy by był całkowicie niezależny referencyjnie od oryginału, a jednocześnie był identyczny jak on. Łatwo można to uzyskać za pomocą niezbyt eleganckiej sztuczki z JSONem. Metoda ta działa tylko z strukturami danych - obiekt w tym wypadku nie może zawierać funkcji.

Osobiście nie stosowałabym tego sposobu - w przypadku gdy przy wstępnych założeniach obiekt jest zwykłą strukturą danych, a później nieświadomy inny programista dopisze do niego metody, pojawi się nieoczekiwany i problematyczny bug.

var strawberry = {
    color: 'red',
    taste: 'sweet',
    size: 'large',
    shape: {
        x: 10,
        y: 32,
        z: 33
    }
};
const someOtherStrawberry = JSON.parse(JSON.stringify(strawberry));
someOtherStrawberry.size = 'small';
console.log(strawberry.size) 
// output: large, wszystko OK!  

someOtherStrawberry.shape.x = 32;
console.log(strawberry.shape.x) 
// output: 10, wszystko OK!

Customowa funkcja do głębokiego kopiowania

Posiadamy już narzędzia do tworzenia płytkich kopii, możemy więc je wykorzystać przy tworzeniu głębokich kopii, wystarczy rekurencyjnie przejść po wszystkich węzłach.

Algorytm sprawdza czy przekazana w argumencie wartość - jeśli jest prymitywem, po prostu zwraca go. W przypadku, gdy argument jest obiektem, przechodzimy po każdym jego polu i sprawdzamy czy jest ono typem prymitywnym, jeśli nie - wywołujemy funkcję "rozbijającą" dla pola nie-prymitywnego .

Wszystko się powtarza do momentu, gdy algorytm przejdzie po całym drzewie.

const deepCopyFunc = child => {
    let parent, value, key
    if (typeof child !== "object" || child === null) {
        return child // zwracamy jeśli jest typem prymitywnym  
    }
    parent = Array.isArray(child) ? [] : {}

    //jeśli jest obiektem to iterujemy po każdym property  
    for (key in child) {
        value = child[key]
        //jeśli wartość dla key jest obiektem to wykonujemy rekurencyjną kopie  
        if (value !== null && typeof value === "object") {
            parent[key] = deepCopyFunc(value)
        } else {
            parent[key] = value
        }
    }
    return parent
}

let originalArray = [37, 3700, {
    hello: "world"
}]

Myślę, że artykuł okazał się pomocny, a moje objaśnienia niezbyt trudne. Jeśli zauważysz gdzieś błąd, proszę, napisz do mnie. Zależy mi, by mój blog był rzetelnym źródłem informacji, więc z chęcią przyjmuję krytykę.