Пошук уроків, статей та іншого контенту
Навчитеся створювати поверхневі копії через spread і slice та зрозумієте межі копіювання вкладених даних.
У JavaScript масив — це об’єкт. Змінна не містить сам масив безпосередньо, а зберігає посилання на нього.
const first = [1, 2, 3];
const second = first;
second.push(4);
console.log(first); // [1, 2, 3, 4]
console.log(second); // [1, 2, 3, 4]
console.log(first === second); // trueОперація присвоєння second = first не створює копію. Обидві змінні посилаються на один об’єкт у пам’яті.
Те саме стосується об’єктів усередині масиву:
const original = [{ name: "Ada" }];
const copy = original;
copy[0].name = "Grace";
console.log(original[0].name); // "Grace"Щоб не змінювати початковий масив під час операцій, часто створюють його копію.
Поверхнева копія створює новий зовнішній масив, але не копіює рекурсивно його вкладені значення.
Для масивів найпоширеніший синтаксис копіювання — оператор розгортання:
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3]
console.log(copy); // [1, 2, 3, 4]
console.log(original === copy); // falseЗовнішні масиви різні, тому push змінює лише copy.
Spread також зручно використовувати для додавання або заміни елементів без мутації:
const users = ["Ada", "Grace"];
const updatedUsers = [
...users,
"Linus"
];
const replacedUsers = [
"Alan",
...users.slice(1)
];
console.log(updatedUsers); // ["Ada", "Grace", "Linus"]
console.log(replacedUsers); // ["Alan", "Grace"]sliceВиклик slice() без аргументів також створює поверхневу копію всього масиву:
const original = [10, 20, 30];
const copy = original.slice();
copy[0] = 100;
console.log(original); // [10, 20, 30]
console.log(copy); // [100, 20, 30]На відміну від spread, slice може одночасно копіювати частину масиву:
const numbers = [0, 1, 2, 3, 4];
const part = numbers.slice(1, 4);
console.log(part); // [1, 2, 3]Для звичайних щільних масивів [...] і slice() часто взаємозамінні. Вони обидва копіюють лише перший рівень.
Розглянемо масив із вкладеним об’єктом:
const original = [
{
id: 1,
profile: {
name: "Ada"
}
}
];
const copy = [...original];
copy[0].profile.name = "Grace";
console.log(original[0].profile.name); // "Grace"
console.log(copy[0].profile.name); // "Grace"
console.log(original === copy); // false
console.log(original[0] === copy[0]); // trueЗовнішні масиви різні:
original !== copyАле елементи масиву — це ті самі об’єкти:
original[0] === copy[0]Тому зміна copy[0].profile.name змінює спільний вкладений об’єкт.
Це можна уявляти як копіювання коробки без копіювання речей усередині:
сама коробка — нова;
посилання на вкладені об’єкти — старі;
примітиви копіюються як значення;
об’єкти, масиви, функції та інші посилальні значення залишаються спільними.
До посилальних значень належать, зокрема:
об’єкти;
масиви;
функції;
Date;
Map;
Set;
екземпляри класів;
регулярні вирази.
Якщо потрібно змінити вкладений об’єкт без мутації початкової структури, потрібно скопіювати кожен рівень, який змінюється:
const users = [
{
id: 1,
profile: {
name: "Ada",
city: "London"
}
}
];
const updatedUsers = users.map((user) => {
if (user.id !== 1) {
return user;
}
return {
...user,
profile: {
...user.profile,
city: "Oxford"
}
};
});
console.log(users[0].profile.city); // "London"
console.log(updatedUsers[0].profile.city); // "Oxford"
console.log(users === updatedUsers); // false
console.log(users[0] === updatedUsers[0]); // falseУ цьому прикладі:
map створює новий масив.
Для потрібного користувача створюється новий об’єкт.
Для profile створюється ще один новий об’єкт.
Інші рівні, які не змінюються, можна залишити спільними.
Такий підхід називають структурним оновленням або структурним спільним використанням. Він не копіює всю структуру без потреби.
Той самий принцип працює для масивів усередині об’єктів:
const projects = [
{
id: 1,
name: "Compiler",
tags: ["javascript", "backend"]
}
];
const updatedProjects = projects.map((project) => {
if (project.id !== 1) {
return project;
}
return {
...project,
tags: [
...project.tags,
"typescript"
]
};
});
console.log(projects[0].tags);
// ["javascript", "backend"]
console.log(updatedProjects[0].tags);
// ["javascript", "backend", "typescript"]Важливо копіювати саме той вкладений масив, який змінюється. Простого копіювання зовнішнього масиву недостатньо.
Примітивні значення не мають спільного змінюваного стану:
const original = [1, "text", true, null];
const copy = [...original];
console.log(original[0] === copy[0]); // trueРезультат true тут не означає, що масиви спільні. Він означає, що значення 1 однакове. Примітиви копіюються за значенням, а не за посиланням.
До примітивів належать:
string;
number;
bigint;
boolean;
undefined;
symbol;
null.
Глибока копія створює незалежні копії вкладених об’єктів і масивів. У сучасному JavaScript для цього можна використовувати structuredClone.
const original = {
title: "Документ",
tags: ["javascript", "arrays"],
metadata: {
createdAt: new Date("2024-01-01T00:00:00Z")
}
};
const copy = structuredClone(original);
copy.tags.push("references");
copy.metadata.createdAt.setUTCFullYear(2025);
console.log(original.tags);
// ["javascript", "arrays"]
console.log(copy.tags);
// ["javascript", "arrays", "references"]
console.log(original.metadata.createdAt.getUTCFullYear());
// 2024
console.log(copy.metadata.createdAt.getUTCFullYear());
// 2025
console.log(original.metadata.createdAt instanceof Date);
// truestructuredClone рекурсивно копіює підтримувані структуровані дані, зокрема:
вкладені об’єкти;
масиви;
циклічні посилання;
Date;
Map;
Set;
типізовані масиви.
Приклад із циклічним посиланням:
const original = {
name: "Node"
};
original.self = original;
const copy = structuredClone(original);
console.log(copy !== original); // true
console.log(copy.self === copy); // trueОднак глибоке копіювання не є універсальною операцією для будь-яких значень. Наприклад, structuredClone не копіює функції:
const source = {
calculate() {
return 42;
}
};
// structuredClone(source); // DataCloneErrorДля класів, функцій, ресурсів середовища виконання та спеціальної поведінки може знадобитися власна логіка копіювання.
JSON.parse(JSON.stringify(...))Іноді глибоку копію намагаються створити так:
const copy = JSON.parse(JSON.stringify(original));Цей підхід не є повноцінним алгоритмом глибокого копіювання. Він спочатку серіалізує дані у JSON, а потім відновлює лише те, що підтримує JSON.
Проблеми можуть виникати з:
Date, які перетворюються на рядки;
undefined, який зникає в об’єктах;
функціями, які зникають;
Map і Set, які не зберігаються коректно;
NaN та Infinity, які перетворюються на null;
BigInt, який спричиняє помилку;
циклічними посиланнями, які спричиняють помилку;
властивостями-символами;
спеціальними екземплярами класів.
Такий спосіб може бути прийнятним лише для простих JSON-сумісних даних, коли його обмеження відомі й бажані.
Використовуйте spread або slice, якщо:
потрібно скопіювати масив на один рівень;
елементи є примітивами;
вкладені об’єкти не змінюватимуться;
потрібно додати, видалити або замінити елемент без мутації зовнішнього масиву.
Використовуйте структурне оновлення, якщо:
змінюється один або кілька відомих вкладених рівнів;
важливо зберегти незмінність початкових даних;
не потрібно дублювати всю структуру.
Використовуйте structuredClone, якщо:
потрібна незалежна копія всієї підтримуваної структури;
дані можуть містити вкладені масиви, об’єкти, Date, Map або Set;
глибоке копіювання справді необхідне.
Оператор === порівнює об’єкти за посиланням, а не за вмістом:
const first = { value: 1 };
const second = { value: 1 };
const third = first;
console.log(first === second); // false
console.log(first === third); // trueОднакові властивості не роблять два об’єкти однаковими. Щоб порівнювати структури за вмістом, потрібен окремий алгоритм або бібліотека, яка враховує типи, порядок властивостей, цикли та інші особливості.
Це важливо, зокрема, у стані застосунку та мемоізації: нове посилання може сигналізувати, що певна частина даних змінилася.
slice і spread для розріджених масивівРозріджений масив містить пропущені індекси:
const sparse = [];
sparse[2] = "value";
const bySlice = sparse.slice();
const bySpread = [...sparse];
console.log(0 in bySlice); // false
console.log(0 in bySpread); // true
console.log(bySlice); // [ <2 empty items>, "value" ]
console.log(bySpread); // [undefined, undefined, "value"]slice зберігає пропущені позиції, а spread проходить масив як ітерований об’єкт і створює на цих позиціях undefined.
Для звичайних щільних масивів ця різниця непомітна, але вона може мати значення для алгоритмів, які перевіряють наявність індексу через оператор in.
const copy = [...original];
copy[0].settings.enabled = false;Якщо settings — вкладений об’єкт, початкові дані також можуть змінитися. Потрібно скопіювати кожен змінюваний рівень.
Object.freeze не створює копію:
const data = [{ value: 1 }];
Object.freeze(data);Він також не заморожує вкладені об’єкти автоматично. Це поверхневе заморожування, а не глибока незмінність.
Глибока копія може бути дорогою для великих структур і непотрібною, якщо змінюється лише один невеликий фрагмент. Часто краще скопіювати лише змінені рівні.
JSON-перетворення втрачає типи та спеціальні значення. Не слід використовувати його як універсальний спосіб клонування.
const забороняє мутаціюconst забороняє переприсвоїти змінну, але не забороняє змінювати об’єкт:
const items = [];
items.push("value"); // дозволено
// items = ["new value"]; // помилкаНезмінність даних потрібно забезпечувати відповідними операціями копіювання та правилами роботи зі станом.
Присвоєння масиву копіює посилання, а не сам масив.
[...] і slice() створюють поверхневі копії.
Поверхнева копія не дублює вкладені масиви та об’єкти.
Примітиви копіюються як значення, а посилальні типи залишаються спільними.
Для безпечного оновлення вкладених даних потрібно копіювати кожен змінюваний рівень.
structuredClone підходить для глибокого копіювання багатьох стандартних типів.
JSON.parse(JSON.stringify(...)) має суттєві обмеження і не є універсальним клонувачем.
=== порівнює об’єкти за посиланням.
Копіювання слід виконувати на потрібну глибину, а не автоматично дублювати всю структуру.