Як розрізнити глибокі та неглибокі копії в 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, щоб дізнатися більше.