Skip to main content

Як скопіювати об'єкт у JavaScript?

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

Теорія

Коротко

  • Поверхнева копія (shallow copy) - як ксерокс листа із закритим конвертом: лист є, але конверт веде до оригінального вмісту
  • Глибока копія (deep copy) - ксерокс всього пакету разом із вмістом конверта
  • Поверхнева дублює тільки верхній рівень; вкладені об'єкти залишаються спільними посиланнями
  • Плоский об'єкт без вкладеності: {...obj}. Вкладені об'єкти, які змінюються незалежно: structuredClone
  • JSON.parse(JSON.stringify(obj)) - глибока копія, але губить функції, Date, Map і undefined

Перший погляд на код

javascript
const original = { user: { name: 'Alice' }, id: 1 }; // Поверхнева - вкладений об'єкт залишається спільним посиланням const shallow = { ...original }; shallow.user.name = 'Bob'; console.log(original.user.name); // "Bob" - оригінал змінився! // Глибока - вкладений об'єкт повністю незалежний const deep = structuredClone(original); deep.user.name = 'Charlie'; console.log(original.user.name); // "Alice" - оригінал в безпеці

Зміна shallow.user.name змінює і original.user.name, бо обидва вказують на один і той самий об'єкт в пам'яті. structuredClone повністю розриває цей зв'язок.

Головна різниця

Поверхнева копія дублює тільки значення верхнього рівня. Примітиви (рядки, числа, булеви) копіюються за значенням і залишаються незалежними. Але вкладені об'єкти та масиви копіюються за посиланням: і оригінал, і копія вказують на ту саму адресу в пам'яті. Змінив вкладені дані через копію - змінив і оригінал. Глибока копія обходить всю структуру рекурсивно і створює нові об'єкти на кожному рівні. Жодних спільних посилань. Мутації залишаються ізольованими.

Коли що використовувати

  • Плоский конфіг без вкладеності: {...obj} або Object.assign({}, obj)
  • React-стан із вкладеними об'єктами: structuredClone(state) перед оновленням
  • Тільки JSON-дані, без Date, функцій і Map: JSON.parse(JSON.stringify(obj))
  • Об'єкт містить Date, Map або Set: structuredClone (Chrome 98+, Node 17.5+)
  • Legacy-середовище або складні класи: lodash.cloneDeep
  • Спільні read-only дані, які ніколи не змінюються: поверхнева копія - швидша і достатня

Таблиця порівняння

МетодТипФункції?Date/Map/Set?Продуктивність
{...obj}ShallowНіНі (копіює посилання)Найшвидший
Object.assign({}, obj)ShallowНіНі (копіює посилання)Швидкий
JSON.parse(JSON.stringify(obj))DeepНі (губить)Ні (Date стає рядком)Середній
structuredClone(obj)DeepНі (кидає помилку)ТакШвидкий
lodash.cloneDeep(obj)DeepТакТакНайповільніший

Як це працює під капотом

Spread і Object.assign перебирають власні enumerable-ключі через внутрішній метод [[GetOwnPropertyKeys]] і копіюють значення за посиланням без рекурсії. Саме тому вони швидкі і зупиняються на першому рівні. JSON.stringify серіалізує об'єкт у рядок, пропускаючи non-enumerable властивості, функції і undefined. Потім JSON.parse відбудовує нове дерево об'єктів. structuredClone використовує алгоритм HTML Structured Clone - той самий механізм, що і в postMessage. Він підтримує Date, Map, Set і Blob, але явно відхиляє функції і WeakMap.

Типові помилки

1. Вважати, що spread копіює вкладені об'єкти

javascript
// Помилка const state = { user: { prefs: {} } }; const copy = { ...state }; copy.user.prefs.dark = true; console.log(state.user.prefs.dark); // true - стан змінився // Виправлення: structuredClone(state)

2. JSON-копія з функціями або undefined

javascript
JSON.stringify({ fn: () => {}, undef: undefined }); // Результат: "{}" - обидві властивості зникли без жодної помилки // Виправлення: structuredClone або lodash.cloneDeep

3. Date перетворюється на рядок після JSON

javascript
const obj = { created: new Date() }; const copy = JSON.parse(JSON.stringify(obj)); console.log(copy.created instanceof Date); // false - це вже рядок // Виправлення: structuredClone зберігає екземпляр Date

4. Object.assign з цільовим об'єктом

javascript
// Помилка - безпосередньо змінює myObject Object.assign(myObject, source); // Виправлення Object.assign({}, myObject, source);

5. Кругові посилання (circular references)

javascript
const obj = {}; obj.self = obj; JSON.stringify(obj); // RangeError: circular structure structuredClone(obj); // DataCloneError // Виправлення: lodash.cloneDeep коректно працює з круговими посиланнями

Де це зустрічається на практиці

  • React: structuredClone(state) для незмінних оновлень вкладеного стану
  • Redux Toolkit: всередині використовує Immer, але structuredClone підходить для міграції серіалізованого стану
  • Express-middleware: const data = structuredClone(req.body), щоб не мутувати оригінальний запит
  • Node.js: spread для злиття конфігів, structuredClone для передачі даних між worker-потоками
  • Lodash: cloneDeep використовується в понад 1000 npm-пакетах як безпечний варіант для legacy-проектів

З мого досвіду, найпоширеніший production-баг з цієї теми - мутація React-стану через поверхневу копію вкладеного об'єкта. Компонент ніби ре-рендериться, але показує застарілі дані, бо посилання на вкладений об'єкт не змінилось.

Питання на співбесіді

Q: Що виведе original.b.c, якщо зробити const copy = { ...{ a: 1, b: { c: 2 } } } і потім copy.b.c = 3?
A: original.b.c стане 3. Spread копіює тільки верхній рівень, тому b залишається спільним посиланням.

Q: Чому JSON.stringify не серіалізує функції?
A: JSON - це текстовий формат за специфікацією RFC 8259. Функції в ньому не передбачені, тому JSON.stringify просто їх пропускає без помилки.

Q: Реалізуй поверхневу копію без вбудованих методів.
A: Object.keys(obj).reduce((acc, k) => { acc[k] = obj[k]; return acc; }, {}) - перебираєш власні ключі і копіюєш значення по одному.

Q: Коли structuredClone кидає помилку?
A: Кидає DataCloneError для типів, які не можна клонувати: WeakMap, WeakSet, функції, DOM-вузли. Також кидає на кругових посиланнях, на відміну від lodash.cloneDeep.

Q: Що відбувається з prototype-властивостями і non-enumerable props при spread?
A: Обидва губляться. Spread копіює тільки власні enumerable-ключі. Якщо оригінал створений через Object.create(proto), копія не матиме прив'язки до прототипу. Властивості, визначені через Object.defineProperty з enumerable: false, теж пропускаються.

Приклади

Пастка поверхневої копії на практиці

javascript
const config = { server: { host: 'localhost', port: 3000 }, timeout: 5000 }; const devConfig = { ...config }; devConfig.timeout = 8000; // OK - примітив, незалежна копія devConfig.server.port = 4000; // Погано - мутує оригінал! console.log(config.timeout); // 5000 - в безпеці console.log(config.server.port); // 4000 - змінено

Зміна timeout безпечна, бо це примітив, а примітиви копіюються за значенням. Зміна server.port мутує оригінал, бо config.server і devConfig.server вказують на один і той самий об'єкт. Щоб захистити вкладені дані, заміни spread на structuredClone(config).

Оновлення React-стану через structuredClone

javascript
const [userData, setUserData] = useState({ profile: { name: 'Alice', settings: { theme: 'dark' } } }); // Неправильно: spread не захищає вкладені settings const badUpdate = { ...userData }; badUpdate.profile.settings.theme = 'light'; setUserData(badUpdate); // стан вже змутовано ще до виклику setUserData // Правильно: structuredClone створює повністю незалежну копію const goodUpdate = structuredClone(userData); goodUpdate.profile.settings.theme = 'light'; setUserData(goodUpdate); // userData.profile.settings.theme залишається 'dark' до наступного рендеру

Неправильний варіант мутує стан ще до виклику setUserData. React може не побачити зміну, бо посилання на profile не змінилось - старий і новий об'єкти стану виглядають однаково для reconciler.

Edge case: прототипи, non-enumerable властивості і кругові посилання

javascript
// Прототип і non-enumerable const original = Object.create({ proto: 'shared' }); original.own = { nest: 'value' }; Object.defineProperty(original, 'hidden', { value: 'secret', enumerable: false }); const spread = { ...original }; console.log(spread.proto); // undefined - прототип загубився console.log(spread.hidden); // undefined - non-enumerable пропущено const json = JSON.parse(JSON.stringify(original)); console.log(json.proto); // undefined const cloned = structuredClone(original); console.log(cloned.proto); // 'shared' - structuredClone зберігає ланцюжок прототипів // Кругове посилання const circular = { name: 'test' }; circular.self = circular; // JSON.stringify(circular) - RangeError // structuredClone(circular) - DataCloneError import _ from 'lodash'; const safe = _.cloneDeep(circular); // справляється коректно

Тільки structuredClone зберігає ланцюжок прототипів з Object.create. Тільки lodash.cloneDeep виживає при кругових посиланнях без помилок. Саме в цих двох ситуаціях spread і JSON або мовчки дають неправильний результат, або кидають виняток.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?