Kako razlikovati duboke i plitke kopije u JavaScript-u

Novo je uvijek bolje!

Sigurno ste se već ranije bavili kopijama u JavaScript-u, čak i ako to niste znali. Možda ste također čuli za paradigmu u funkcionalnom programiranju da ne biste trebali mijenjati postojeće podatke. Da biste to učinili, morate znati kako sigurno kopirati vrijednosti u JavaScript. Danas ćemo pogledati kako to učiniti, izbjegavajući zamke!

Prije svega, što je kopija?

Kopija samo izgleda kao stara stvar, ali nije. Kada promijenite kopiju, očekujete da će izvorna stvar ostati ista, dok se kopija mijenja.

U programiranju vrijednosti pohranjujemo u varijable. Kopiranje znači da pokrećete novu varijablu s istim vrijednostima. Međutim, postoji velika potencijalna zamka koju treba razmotriti: duboko kopiranje nasuprot plitkom kopiranju . Dubinska kopija znači da su sve vrijednosti nove varijable kopirane i odvojene od izvorne varijable. Plitka kopija znači da su određene (pod) vrijednosti još uvijek povezane s izvornom varijablom.

Da biste stvarno razumjeli kopiranje, morate saznati kako JavaScript pohranjuje vrijednosti.

Primitivni tipovi podataka

Primitivne vrste podataka uključuju sljedeće:

  • Broj - npr 1
  • Niz - npr 'Hello'
  • Booleova - npr true
  • undefined
  • null

Kada kreirate ove vrijednosti, one su usko povezane s varijablom kojoj su dodijeljene. Oni postoje samo jednom. To znači da se zapravo ne morate brinuti o kopiranju primitivnih vrsta podataka u JavaScript. Kad napravite kopiju, to će biti prava kopija. Pogledajmo primjer:

const a = 5
let b = a // this is the copy
b = 6
console.log(b) // 6
console.log(a) // 5

Izvršenjem b = aizrađujete kopiju. Sada, kada dodijelite novu vrijednost b, vrijednost se bmijenja, ali ne i a.

Sastavljeni tipovi podataka - Objekti i nizovi

Tehnički, nizovi su također objekti, pa se ponašaju na isti način. Kasnije ću detaljno proći kroz obojicu.

Ovdje postaje zanimljivije. Te se vrijednosti zapravo pohranjuju samo jednom kada se uspostave, a dodjeljivanje varijable samo stvara pokazivač (referencu) na tu vrijednost .

Sada, ako ćemo napraviti kopiju b = ai promijeniti neke ugnežđeni vrijednost u b, to je zapravo mijenja a„s ugnežđeni vrijednost kao dobro, jer ate bzapravo ukazuju na istu stvar. Primjer:

const a = {
 en: 'Hello',
 de: 'Hallo',
 es: 'Hola',
 pt: 'Olà'
}
let b = a
b.pt = 'Oi'
console.log(b.pt) // Oi
console.log(a.pt) // Oi

U gornjem primjeru zapravo smo napravili plitku kopiju . To je često problematično, jer očekujemo da će stara varijabla imati izvorne vrijednosti, a ne promijenjene. Kad mu pristupimo, ponekad dobijemo pogrešku. Moglo bi se dogoditi da ga pokušate otkloniti neko vrijeme prije nego što pronađete pogrešku, jer mnogi programeri zapravo ne shvaćaju koncept i ne očekuju da je to pogreška.

Pogledajmo kako možemo napraviti kopije objekata i nizova.

Predmeti

Postoji više načina za kopiranje objekata, posebno s novom proširivanjem i poboljšanjem JavaScript specifikacije.

Operater širenja

Predstavljen s ES2015, ovaj je operater sjajan, jer je tako kratak i jednostavan. Sve vrijednosti se 'šire' u novi objekt. Možete ga koristiti na sljedeći način:

const a = {
 en: 'Bye',
 de: 'Tschüss'
}
let b = {...a}
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss

Možete ga koristiti i za spajanje dvaju objekata, na primjer const c = {...a, ...b}.

Objekt.dodijeliti

To se uglavnom koristilo prije nego što je operater širenja bio u blizini, i u osnovi radi istu stvar. Morate biti oprezni, jer se prvi argument u Object.assign()metodi zapravo mijenja i vraća. Pazite da objekt proslijedite kopiranju barem kao drugi argument. Obično biste samo proslijedili prazan objekt kao prvi argument kako biste spriječili izmjenu postojećih podataka.

const a = {
 en: 'Bye',
 de: 'Tschüss'
}
let b = Object.assign({}, a)
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss

Zamka: ugniježđeni predmeti

Kao što je već spomenuto, postoji jedno veliko upozorenje kada se radi s kopiranjem objekata, a odnosi se na obje gore navedene metode. Kada imate ugniježđeni objekt (ili niz) i kopirate ga, ugniježđeni objekti unutar tog objekta neće se kopirati, jer su to samo pokazivači / reference. Stoga, ako promijenite ugniježđeni objekt, promijenit ćete ga za obje instance, što znači da biste na kraju ponovno napravili plitku kopiju . Primjer: // LOŠ PRIMJER

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = {...a}
b.foods.dinner = 'Soup' // changes for both objects
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Soup

Da biste napravili dubinsku kopiju ugniježđenih objekata , to biste trebali uzeti u obzir. Jedan od načina da se to spriječi je ručno kopiranje svih ugniježđenih objekata:

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = {foods: {...a.foods}}
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

U slučaju da ste se pitali što učiniti kada objekt ima više ključeva nego samo foods, možete iskoristiti puni potencijal operatora širenja. Kada prosljeđuju više svojstava nakon ...spread, prepisuju izvorne vrijednosti, na primjer const b = {...a, foods: {...a.foods}}.

Izrada dubokih kopija bez razmišljanja

Što ako ne znate koliko su duboko ugniježđene strukture? Može biti vrlo zamorno ručno prolaziti kroz velike objekte i ručno kopirati svaki ugniježđeni objekt. Postoji način da sve kopirate bez razmišljanja. Jednostavno stringifysvoj objekt i parseto odmah nakon:

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = JSON.parse(JSON.stringify(a))
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

Ovdje morate uzeti u obzir da nećete moći kopirati instance prilagođene klase, pa ga možete koristiti samo kada kopirate objekte s izvornim JavaScript vrijednostima .

Nizovi

Kopiranje nizova jednako je često kao i kopiranje objekata. Puno logike iza toga je slično, jer su nizovi također samo objekti ispod haube.

Operater širenja

Kao i kod objekata, za kopiranje niza možete koristiti operator širenja:

const a = [1,2,3]
let b = [...a]
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

Funkcije niza - mapiranje, filtriranje, smanjenje

Te će metode vratiti novi niz sa svim (ili nekim) vrijednostima izvornog. Dok to radite, možete i mijenjati vrijednosti, što vam vrlo dobro dođe:

const a = [1,2,3]
let b = a.map(el => el)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

Alternativno možete promijeniti željeni element tijekom kopiranja:

const a = [1,2,3]
const b = a.map((el, index) => index === 1 ? 4 : el)
console.log(b[1]) // 4
console.log(a[1]) // 2

Niz.sječak

Ova se metoda obično koristi za vraćanje podskupa elemenata, počevši od određenog indeksa i po želji završavajući određenim indeksom izvornog polja. Kada koristite array.slice()ili array.slice(0)ćete završiti s kopijom izvornog niza.

const a = [1,2,3]
let b = a.slice(0)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

Ugniježđeni nizovi

Slično objektima, upotreba gornjih metoda za kopiranje niza s drugim nizom ili objektom unutar njih generirat će plitku kopiju . Da biste to spriječili, također upotrijebite JSON.parse(JSON.stringify(someArray)).

BONUS: kopiranje instance prilagođenih klasa

Kada ste već profesionalac u JavaScript-u i bavite se svojim prilagođenim konstruktorskim funkcijama ili klasama, možda želite kopirati i instance takvih.

Kao što je već spomenuto, ne možete ih samo raščlaniti + raščlaniti jer ćete izgubiti metode predavanja. Umjesto toga, željeli biste dodati prilagođenu copymetodu za stvaranje nove instance sa svim starim vrijednostima. Pogledajmo kako to funkcionira:

class Counter {
 constructor() {
 this.count = 5
 }
 copy() {
 const copy = new Counter()
 copy.count = this.count
 return copy
 }
}
const originalCounter = new Counter()
const copiedCounter = originalCounter.copy()
console.log(originalCounter.count) // 5
console.log(copiedCounter.count) // 5
copiedCounter.count = 7
console.log(originalCounter.count) // 5
console.log(copiedCounter.count) // 7

Da biste se bavili objektima i nizovima na koje se poziva vaša instanca, morali biste primijeniti svoje novo naučene vještine o dubinskom kopiranju ! Samo ću dodati konačno rješenje za prilagođenu konstruktorsku copymetodu kako bi bila dinamičnija:

Pomoću te metode kopiranja možete u svoj konstruktor staviti onoliko vrijednosti koliko želite, a da ne morate ručno kopirati sve!

O autoru: Lukas Gisder-Dubé suosnovao je i vodio startup kao CTO tijekom 1 1/2 godine, gradeći tehnički tim i arhitekturu. Nakon napuštanja startupa, predavao je kodiranje kao vodeći instruktor u Ironhacku, a sada gradi Startup agenciju i savjetovanje u Berlinu. Pogledajte dube.io da biste saznali više.