Как се прави разлика между дълбоки и плитки копия в JavaScript

Новото винаги е по-добро!

Със сигурност сте имали работа с копия в JavaScript и преди, дори и да не сте го знаели. Може би сте чували и за парадигмата във функционалното програмиране, че не трябва да променяте съществуващи данни. За да направите това, трябва да знаете как безопасно да копирате стойности в JavaScript. Днес ще разгледаме как да направим това, като същевременно избягваме капаните!

На първо място, какво е копие?

Копие просто прилича на старото нещо, но не е така. Когато сменяте копието, очаквате оригиналното нещо да остане същото, докато копието се променя.

При програмирането съхраняваме стойности в променливи. Направата на копие означава, че вие ​​инициирате нова променлива със същите стойности. Има обаче голям потенциален подводен камък, който трябва да се разгледа: дълбоко копиране срещу плитко копиране . Дълбоко копиране означава, че всички стойности на новата променлива се копират и прекъсват връзката с оригиналната променлива. Плитко копие означава, че определени (под) стойности все още са свързани с оригиналната променлива.

За да разберете наистина копирането, трябва да разберете как JavaScript съхранява стойности.

Примитивни типове данни

Примитивните типове данни включват следното:

  • Номер - напр 1
  • Низ - напр 'Hello'
  • Булево - напр true
  • undefined
  • null

Когато създавате тези стойности, те са тясно свързани с променливата, към която са назначени. Те съществуват само веднъж. Това означава, че всъщност не трябва да се притеснявате за копиране на примитивни типове данни в JavaScript. Когато направите копие, то ще бъде истинско копие. Да видим пример:

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

Изпълнявайки b = a, вие правите копието. Сега, когато преназначавате нова стойност на b, стойността на bпромените, но не на a.

Композитни типове данни - Обекти и масиви

Технически масивите също са обекти, така че те се държат по същия начин. По-късно ще разгледам подробно и двете.

Тук става по-интересно. Тези стойности всъщност се съхраняват само веднъж, когато се създават екземпляри и присвояването на променлива просто създава указател (препратка) към тази стойност .

Сега, ако направим копие b = aи променим някаква вложена стойност b, това всъщност променя aи вложената стойност, тъй aкато bвсъщност сочи към едно и също нещо. Пример:

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

В горния пример всъщност направихме плитко копие . Това често е проблематично, тъй като очакваме старата променлива да има първоначалните стойности, а не променените. Когато имаме достъп до него, понякога получаваме грешка. Може да се случи да опитате да го отстраните за известно време, преди да откриете грешката, тъй като много разработчици наистина не разбират концепцията и не очакват това да е грешката.

Нека да разгледаме как можем да правим копия на обекти и масиви.

Обекти

Има множество начини за копиране на обекти, особено с новата разширяваща се и подобряваща спецификация на JavaScript.

Оператор за разпространение

Представен с ES2015, този оператор е просто страхотен, защото е толкова кратък и опростен. Той „разпространява“ всички стойности в нов обект. Можете да го използвате, както следва:

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

Можете също така да го използвате, за да обедините два обекта заедно, например const c = {...a, ...b}.

Обект.присвояване

Това се използваше преди операторът за разпространение да е наоколо и всъщност прави същото. Трябва да бъдете внимателни, тъй като първият аргумент в Object.assign()метода всъщност се модифицира и връща. Затова се уверете, че сте предали обекта за копиране поне като втория аргумент. Обикновено просто бихте предали празен обект като първи аргумент, за да предотвратите промяна на съществуващите данни.

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

Подводни камъни: Вложени обекти

Както бе споменато по-горе, има едно голямо предупреждение при работа с копиране на обекти, което се отнася и за двата метода, изброени по-горе. Когато имате вложен обект (или масив) и го копирате, вложени обекти вътре в този обект няма да бъдат копирани, тъй като те са само указатели / препратки. Следователно, ако промените вложения обект, ще го промените и за двата случая, което означава, че в крайна сметка ще направите плитко копие отново . Пример: // ЛОШ ПРИМЕР

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

За да направите дълбоко копие на вложени обекти , трябва да помислите за това. Един от начините за предотвратяване е ръчното копиране на всички вложени обекти:

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

В случай, че се чудите какво да правите, когато обектът има повече ключове, отколкото само foods, можете да използвате пълния потенциал на оператора за разпространение. Когато предават повече свойства след ...spread, те презаписват първоначалните стойности, например const b = {...a, foods: {...a.foods}}.

Правете дълбоки копия, без да мислите

Ами ако не знаете колко дълбоко са вложените структури? Може да бъде много досадно ръчно преминаване през големи обекти и копиране на всеки вложен обект на ръка. Има начин да копирате всичко, без да мислите. Вие просто stringifyси обект и parseто веднага след:

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

Тук трябва да имате предвид, че няма да можете да копирате потребителски екземпляри на клас, така че можете да го използвате само когато копирате обекти с вътрешни JavaScript стойности вътре.

Масиви

Копирането на масиви е също толкова често срещано, колкото и копирането на обекти. Голяма част от логиката зад него е подобна, тъй като масивите също са само обекти под капака.

Оператор за разпространение

Както при обектите, можете да използвате оператора за разпространение, за да копирате масив:

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

Функции на масива - картографиране, филтриране, намаляване

Тези методи ще върнат нов масив с всички (или някои) стойности на оригиналния. Докато правите това, можете също да променяте стойностите, което е много полезно:

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

Като алтернатива можете да промените желания елемент по време на копиране:

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

Array.slice

Този метод обикновено се използва за връщане на подмножество от елементи, започвайки от определен индекс и по избор завършващ с определен индекс на оригиналния масив. Когато използвате array.slice()или array.slice(0)ще получите копие на оригиналния масив.

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

Вложени масиви

Подобно на обектите, използването на методите по-горе за копиране на масив с друг масив или обект вътре ще генерира плитко копие . За да предотвратите това, използвайте също JSON.parse(JSON.stringify(someArray)).

БОНУС: копиране на екземпляр от потребителски класове

Когато вече сте професионалист в JavaScript и се занимавате с вашите персонализирани конструкторски функции или класове, може би искате да копирате и копия на тези.

Както споменахме по-горе, не можете просто да ги анализирате + анализирате, тъй като ще загубите методите на класа си. Вместо това бихте искали да добавите персонализиран copyметод, за да създадете нов екземпляр с всички стари стойности. Нека да видим как работи това:

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

За да се справите с обекти и масиви, на които има препратки във вашия екземпляр, ще трябва да приложите новите си научени умения за дълбоко копиране ! Просто ще добавя окончателно решение за copyметода на потребителския конструктор , за да го направя по-динамичен:

С този метод на копиране можете да поставите колкото искате стойности във вашия конструктор, без да се налага да копирате ръчно всичко!

Относно автора: Лукас Гисдер-Дубе е съосновател и ръководи стартъп като технически директор за 1 1/2 години, изграждайки техническия екип и архитектура. След като напусна старта, той преподава кодиране като водещ инструктор в Ironhack и сега изгражда Startup Agency & Consultancy в Берлин. Разгледайте dube.io, за да научите повече.