Referencja i kopiowanie obiektów

Kopiowanie obiektów

Tak jak już wspominaliśmy sobie w dziale o typach danych, typy złożone (obiekty) charakteryzują się tym, że jeżeli pod dwie i więcej zmiennych podstawimy ten sam obiekt, będą one wskazywać na ten sam byt:


const a = { name : "kot" }
const b = a;

b.age = 5;

console.log(a, b); //{name: "kot", age: 5}, {name: "kot", age: 5}

Jeżeli byśmy chcieli dodawać nowe właściwości tylko do zmiennej b, powinna ona zawierać kopię obiektu a. Taki duplikat możemy zrobić na kilka sposobów.

Zanim przejdziemy dalej, rozważmy przykład:


//mamy tablicę która chcemy posortować
const tab = [4, 10, 2, 11, 1.1, 3];

//piszemy odpowiednią funkcję
const sortNumbers = function(arr) {
    return arr.sort((a, b) => a - b);
}

const tabSorted = sortNumbers(tab);
console.log(tabSorted); //[1.1, 2, 3, 4, 10, 11]
console.log(tab); //[1.1, 2, 3, 4, 10, 11]

Nasza funkcja powinna zwrócić posortowaną tablicę. I robi to. Problem w tym, że dodatkowo modyfikowana jest oryginalna tablica.

Czemu tak się dzieje? Pamiętaj, że jeżeli przekazujesz do funkcji pod dany parametr jakiś obiekt, we wnętrzu tej funkcji występuje on pod nazwą parametru. Nie sprawia to jednak, że magicznie jest to kopia przekazanych danych - to wciąż ten sam obiekt. Można to przyrównać do sytuacji z pierwszego listingu. Dany obiekt (w tym przypadku tablica) po przekazaniu go jako argument występuje pod dwoma nazwami tab i arr. Funkcja sort modyfikuje tablicę, dlatego obie zmienne zostały zmienione.

I teraz wyobraź sobie, że piszesz jakąś dużą aplikację, gdzie na początku pobierasz tablicę użytkowników z serwera. Tablica taka ma konkretną kolejność, która z jakiegoś powodu będzie dla ciebie później ważna. Napisałeś kilka funkcji, które wykonują na tej tablicy różne czynności. Gdyby każda z nich modyfikowała początkową tablicę, bardzo szybko byś się zagubił.

Dlatego też dobrym pomysłem jest pisać funkcje tak, by zwracały nowe dane utworzone na bazie tych które im przekazano i nie nie modyfikowały tych przekazanych. Czyli w skrócie - by działały na kopiach danych.

W przypadku tablic warto zainteresować się nowymi metodami, które w odróżnieniu od ich starszych odpowiedników nie modyfikują oryginalnej tablicy, a tylko zwracają nową wersję.

W najnowszym Javascripcie do kopiowania płaskich obiektów możemy wykorzystać spread syntax:


const a = {
    name : "kot",
    age: 3
}

const b = { ...a }
b.name = "pies"

console.log(a, b) //{name: "kot", age: 3}, {name: "pies", age: 3}

const a = {
    name : "kot",
    speed: 10
}
const b = {
    name : "pies",
    age : 5
}

const c = { ...a, ...b }
console.log(c); //{name : "pies", speed: 10, age: 5}

Możemy też zastosować funkcję Object.assign(cel, ...źródła).

Funkcja ta służy do kopiowania wszystkich wyliczalnych właściwości z jednego lub więcej obiektów do obiektu docelowego.


const a = {
    name : "kot",
    age: 7,
    speed: 10
}

const b = {
    name : "pies",
    age : 5
}

const c = Object.assign({}, a, b);

console.log(c); //{name : "pies", age: 5, speed: 10}

const o1 = { a: 1, b: 1, c: 1 };
const o2 = { b: 2, c: 2 };
const o3 = { c: 3 };

const obj = Object.assign({}, o1, o2, o3);
console.log(obj); //{ a: 1, b: 2, c: 3 }

Powyższymi metodami (spread i assign) możemy skopiować tylko płaskie obiekty. Jeżeli któraś z właściwości wskazuje na inny obiekt, zostanie skopiowana tylko referencja do tego obiektu:


const a = {
    name : "kot",
    food : { type : "fish" }
}

const b = { ...a }
b.food.name = "karp";

console.log(a.food, b.food); //{type : "fish", name : "karp"}, {type : "fish", name : "karp"}

Żeby sklonować obiekt głęboko, musimy zastosować inne metody.

Jedną z najczęściej spotykanych jest użycie hacku - w tym przypadku skorzystanie z obiektu JSON.


const ob = {
    name : "Marcin",
    pet : {
        name : "Feliks",
        kind : "cat"
    }
}

const ob2 = JSON.parse(JSON.stringify(ob));

ob2.pet.name = "Super Szamson";
ob2.pet.kind = "pies";

console.log(ob.pet.name, ob2.pet.name); //Feliks, Super Szamson
console.log(ob.pet.kind, ob2.pet.kind); //cat, pies

Ale nawet to rozwiązanie nie jest w 100% bezpieczne. Standard JSON, a więc i dane o niego oparte są przeznaczone do przechowywania danych, ale nie funkcji. Jeżeli konwertowany obiekt będzie taką posiadał, nie zostanie ona skonwertowana na format JSON.

Zamiast powyższego sposobu wielu programistów sięga po bibliotekę Lodash i jedną z jej metod: https://lodash.com/docs/4.17.4#cloneDeep.

W czystym Javascript od jakiegoś czasu mamy też dostęp do funkcji structuredClone(obToCopy, transfer), która służy właśnie do głębokiego klonowania obiektów.

Pierwszym jej parametrem jest obiekt, których chcemy głęboko skopiować. Drugi opcjonalny parametr pozwala nam podać tak zwany "transferable object", czyli dane, które zamiast kopiowania zostaną przeniesione do nowej struktury.


const ob = {
    name : "Marcin",
    pet : {
        name : "Feliks",
        kind : "cat"
    }
}

const ob2 = structuredClone(ob);
ob2.pet.name = "Maksio";

console.log(ob); //{name: 'Feliks', kind: 'cat'}
console.log(ob2); //{name: 'Maksio', kind: 'cat'}

Ale i ten sposób nie jest pozbawiony wad. Kopiowany obiekt nie może posiadać funkcji, po skopiowaniu straci swój prototyp, a i niektóre typy danych nie dadzą się w ten sposób skopiować. Mimo wszystko jest to dobry wybór w takich sytuacjach (na szczęście są to marginalne sytuacje).

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.