Глибоке копіювання

Глибока копія об'єкта – це копія, чиї властивості не поділяють посилань (що вказують на ті самі значення) з властивостями об'єкта-джерела, з якого була зроблена копія. Як наслідок, коли змінити або джерело, або копію, то можна мати певність, що інший об'єкт змінюватися не буде. Така логіка відрізняється від логіки поверхневого копіювання, в якій зміни вкладених властивостей у джерелі або копії можуть призвести також до змін іншого об'єкта.

Два об'єкти o1 і o2 є структурно еквівалентними, якщо їхня видима поведінка – однакова. Ця поведінка включає наступне:

  1. Властивості o1 і o2 мають одні й ті ж значення в тому самому порядку.
  2. Значення їхніх властивостей є структурно еквівалентними.
  3. Їхні ланцюжки прототипів структурно еквівалентні (хоча, коли розглядається структурна еквівалентність, то об'єкти зазвичай є простими об'єктами, тобто вони обидва успадковують від Object.prototype).

Структурно еквівалентні об'єкти можуть бути або одним і тим же об'єктом (o1 === o2), або копіями (o1 !== o2). Оскільки еквівалентні примітивні значення завжди однакові при порівнянні, то їх не можна скопіювати.

Глибокі копії можна більш формально визначити так:

  1. Вони не є одним об'єктом (o1 !== o2).
  2. Властивості o1 і o2 мають однакові назви в тому самому порядку.
  3. Значення їхніх властивостей є глибокими копіями одне одних.
  4. Їхні ланцюжки прототипів структурно еквівалентні.

Глибокі копії можуть мати або не мати скопійовані ланцюжки прототипів (і нерідко не мають). Але два об'єкти зі структурно нееквівалентними ланцюжками прототипів (наприклад, один є масивом, а інший – простим об'єктом) ніколи не є копіями одне одного.

Копія об'єкта, всі властивості якого мають примітивні значення, вписується у визначення як глибокої копії, так і поверхневої копії. Проте дещо безглуздо говорити про глибину такої копії, адже вона не має вкладених властивостей, а зазвичай про глибоке копіювання говорять у контексті змін вкладених властивостей.

У JavaScript стандартні вбудовані операції копіювання об'єктів (синтаксис розгортання, Array.prototype.concat(), Array.prototype.slice(), Array.from() і Object.assign()) не створюють глибоких копій (замість цього вони створюють поверхневі копії).

Один зі способів створити глибоку копію об'єкта JavaScript, якщо він може бути серіалізований, – скористатися JSON.stringify() для перетворення об'єкта на рядок JSON, а потім JSON.parse() для перетворення рядка назад на (геть новий) об'єкт JavaScript:

const ingredientsList = ["локшина", { list: ["яйця", "борошно", "вода"] }];
const ingredientsListDeepCopy = JSON.parse(JSON.stringify(ingredientsList));

Оскільки глибока копія не поділяє посилань зі своїм об'єктом-джерелом, то всі зміни, що вносяться до глибокої копії, не впливають на об'єкт-джерело.

// Змінити значення властивості 'list' в ingredientsListDeepCopy.
ingredientsListDeepCopy[1].list = ["рисове борошно", "вода"];
// Властивість 'list' не змінюється в ingredients_list.
console.log(ingredientsList[1].list);
// Array(3) [ "яйця", "борошно", "вода" ]

Проте попри те, що об'єкт у коді вище достатньо простий, щоб бути серіалізованим, чимало об'єктів JavaScript не можна серіалізувати взагалі – наприклад, функції (із їхніми замиканнями), символи, об'єкти, що представляють елементи HTML в API DOM HTML, рекурсивні дані та багато інших ситуацій. Виклик JSON.stringify() для серіалізації об'єктів у таких випадках призведе до помилки. Тому немає способу створити глибокі копії таких об'єктів.

API Вебу structuredClone() також створює глибокі копії, маючи при цьому перевагу в тому, що дозволяє переносним об'єктам у джерелі переноситися до нової копії, а не просто клонуватися. Він також обробляє більше типів даних, наприклад, Error. Але слід зауважити, що structuredClone() не є можливістю мови JavaScript – це можливість браузерів та інших середовищ виконання JavaScript, які мають реалізацію API Вебу. І виклик structuredClone() для клонування несеріалізовного об'єкта призведе до помилки так само, як і виклик JSON.stringify() для серіалізації.

Дивіться також