Skip to main content

Поверхневе копіювання проти глибокого копіювання в JavaScript

Поверхневе копіювання (shallow copy) створює новий об'єкт із копіями властивостей верхнього рівня, але вкладені об'єкти залишаються спільними. Глибоке копіювання (deep copy) клонує всю структуру рекурсивно - оригінал і копія не мають спільних посилань.

Теорія

Коротко

  • Поверхнева копія - це як ксерокопія листа з доданими фото: лист новий, але обидва вказують на ті самі фото
  • Глибока копія копіює і фото теж - нічого спільного
  • { ...obj } і Object.assign() - поверхневі; structuredClone() і lodash.cloneDeep() - глибокі
  • Плоский об'єкт без вкладень? Поверхневе копіювання достатньо. Вкладений стан або відповіді API? Потрібне глибоке.
  • structuredClone() - сучасний стандарт для глибокого копіювання, доступний у всіх браузерах з 2022 року та Node.js 17+

Швидкий приклад

javascript
const original = { name: 'Alice', scores: [90, 85] }; const shallow = { ...original }; // поверхнева копія const deep = structuredClone(original); // глибока копія shallow.name = 'Bob'; shallow.scores[0] = 100; console.log(original.name); // 'Alice' - рядок скопійовано за значенням console.log(original.scores[0]); // 100 - масив спільний! console.log(deep.scores[0]); // 90 - повністю незалежний

shallow.name і original.name незалежні, бо рядки - примітиви, вони копіюються за значенням. Але shallow.scores і original.scores вказують на один і той самий масив у пам'яті, тому будь-яка зміна відображається в обох.

Ключова різниця

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

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

  • Конфігураційний об'єкт без вкладень - поверхневе копіювання через { ...obj } або Object.assign
  • Стан React-компонента з вкладеними полями на зразок address.city або масивом координат - глибоке копіювання, щоб ізолювати оновлення
  • Відповідь API, яку кешуєш і потім модифікуєш - structuredClone захистить кеш від мутацій
  • Цикли з великою кількістю ітерацій, де вкладені дані тільки читаються - поверхневе копіювання швидше (перебір верхнього рівня проти рекурсивного обходу)

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

МетодТипФункціїDateЦиклічні посиланняПідходить для
{ ...obj }ПоверхневезбереженізбереженіН/ДПлоскі об'єкти, спред стану в React
Object.assign()ПоверхневезбереженізбереженіН/ДЗлиття плоских конфігів
structuredClone()Глибокекидає помилкузбереженийобробляєСучасні проекти, Node 17+, Worker
JSON.parse/stringifyГлибокевидаляєстає рядкомкидає помилкуПрості об'єкти без спецтипів
lodash.cloneDeep()ГлибокеклонуєзбереженийобробляєЛегасі-код, складні вкладені дані

Як це працює у V8

V8 зберігає об'єкти як хеш-мапи з дескрипторами властивостей і вказівниками. Оператор спред ітерує власні перелічувані властивості через внутрішній метод [[GetOwnProperty]], примітиви копіює прямо, а для вкладених об'єктів просто перепризначає вказівники без клонування. structuredClone() використовує алгоритм HTML Structured Clone: обходить усе дерево, на кожному рівні створює нові об'єкти і відстежує циклічні посилання через внутрішню карту. Саме тому він кидає помилку на функціях - вони не серіалізуються цим алгоритмом. lodash.cloneDeep відстежує вже відвідані вузли через стек і створює нові об'єкти через Object.create(null), що робить його найгнучкішим варіантом для нестандартних ситуацій.

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

1. Припущення, що спред копіює вкладені об'єкти

javascript
const state = { user: { prefs: [1, 2] } }; const copy = { ...state }; copy.user.prefs.push(3); console.log(state.user.prefs); // [1, 2, 3] - оригінал змінено

Спред обробляє тільки перший рівень. copy.user і state.user - це одне і те саме посилання. Виправлення: const copy = structuredClone(state).

2. JSON.parse/stringify з Date або функціями

javascript
const obj = { name: 'Alice', created: new Date(), greet: () => 'hi' }; const copy = JSON.parse(JSON.stringify(obj)); console.log(typeof copy.created); // 'string' - Date став рядком console.log(copy.greet); // undefined - функція зникла

structuredClone зберігає Date коректно. Для функцій вбудованого рішення немає - використовуй lodash.cloneDeep або власну рекурсивну функцію.

3. Думати, що .slice() обробляє вкладені масиви

javascript
const arr = [1, [2, 3]]; const copy = arr.slice(); // поверхневе - вкладені масиви спільні copy[1][0] = 99; console.log(arr[1][0]); // 99 - оригінал змінено

.slice(), спред і Array.from - усі три роблять поверхневу копію масиву. Вкладені масиви жоден з них не торкається.

4. Циклічні посилання ламають JSON

javascript
const obj = { name: 'test' }; obj.self = obj; JSON.parse(JSON.stringify(obj)); // Throws: Converting circular structure to JSON structuredClone(obj); // Працює нормально

Якщо структура даних може містити самопосилання, structuredClone або lodash.cloneDeep - єдині надійні варіанти.

5. Неперелічувані властивості зникають при спреді

javascript
const obj = { a: 1 }; Object.defineProperty(obj, 'b', { value: 2, enumerable: false }); const copy = { ...obj }; console.log(copy.b); // undefined

Спред і Object.assign пропускають неперелічувані властивості. Легко не помітити, коли копіюєш об'єкти з прихованими метаданими, доданими через defineProperty.

Де зустрічається у реальному коді

  • React: спред для плоских оновлень стану ({ ...user, name: 'Bob' }); structuredClone для вкладених даних форми перед відправкою
  • Redux Toolkit: createSlice глибоко клонує початковий стан всередині, щоб захистити сховище від випадкових мутацій
  • next-auth.js: використовує lodash.cloneDeep для об'єктів сесії, які проходять через middleware
  • Express: express-validator клонує req.body перед валідацією - поверхнево для плоских тіл запиту, глибоко для вкладених схем
  • Node.js workers: structuredClone - стандартний спосіб передати дані в Worker-потік без спільної пам'яті

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

Q: Що виведе цей код? const a = [1]; const b = [a]; const c = [...b]; c[0][0] = 99; console.log(a[0]);
A: 99. Спред на b поверхневий, тому c[0] і a - один і той самий масив. Мутація проходить аж до a.

Q: Що робить structuredClone з функціями?
A: Кидає DataCloneError. Функції не серіалізуються алгоритмом Structured Clone. Якщо об'єкт містить функції, використовуй lodash.cloneDeep або власну рекурсивну реалізацію.

Q: Як lodash.cloneDeep обробляє циклічні посилання?
A: Відстежує вже відвідані об'єкти через стек. Коли зустрічає вже клонований об'єкт, повторно використовує його замість нескінченної рекурсії. JSON тут просто падає; structuredClone вирішує це через внутрішню карту посилань.

Q: Чим Object.assign({}, obj) відрізняється від { ...obj }?
A: Для плоских об'єктів результат однаковий. Різниця: Object.assign викликає сетери на цільовому об'єкті, якщо вони є; спред - ні. Обидва викликають гетери на джерелі.

Q: Треба передати глибоко вкладене дерево з 10 000 вузлів у Web Worker. Що використати і чому?
A: Передай напряму через postMessage - воркери автоматично використовують алгоритм Structured Clone, той самий що і structuredClone. JSON-серіалізація втрачає типи для Date, Map, Set і ArrayBuffer. Для справді великих даних варто розглянути Transferable-об'єкти для передачі без копіювання.

Приклади

Мутація поверхневої копії у стані React

javascript
const [user, setUser] = useState({ name: 'Alice', address: { city: 'NY', coords: [40.7, -74.0] } }); // Спред створює новий об'єкт верхнього рівня, але address - спільний const updated = { ...user, name: 'Bob' }; setUser(updated); // Десь далі в коді хтось мутує updated.address updated.address.city = 'LA'; console.log(user.address.city); // 'LA' - оригінальний стан теж змінився

Це одна з найпоширеніших помилок у React-застосунках. Спред { ...user } створює новий об'єкт верхнього рівня, але updated.address і user.address - одна і та сама ділянка пам'яті. Будь-яка мутація вкладеного об'єкта впливає на обидва.

Глибоке копіювання через structuredClone

javascript
const [user, setUser] = useState({ name: 'Alice', address: { city: 'NY', coords: [40.7, -74.0] } }); // Повністю ізольована копія - нічого спільного const updated = structuredClone(user); updated.address.city = 'LA'; updated.address.coords[0] = 34.0; setUser(updated); console.log(user.address.city); // 'NY' - оригінал не змінився console.log(user.address.coords[0]); // 40.7 - оригінал не змінився

structuredClone обходить усе дерево. Date, Map, Set і ArrayBuffer зберігаються з правильними типами. Єдине, що треба пам'ятати: якщо об'єкт містить функції, буде DataCloneError.

Циклічні посилання і ланцюг прототипів (рівень senior)

javascript
const original = { name: 'test' }; original.self = original; // циклічне посилання // JSON одразу падає try { JSON.parse(JSON.stringify(original)); } catch (e) { console.log(e.message); // Converting circular structure to JSON } // structuredClone обробляє коректно const cloned = structuredClone(original); console.log(cloned !== original); // true - різні об'єкти console.log(cloned.self === cloned); // true - self вказує всередину клону console.log(cloned.self !== original); // true - зв'язку з оригіналом немає

Циклічне посилання в cloned вказує назад на cloned, а не на оригінал. Це правильна поведінка для справжньої глибокої копії. Бачив, як це ставить у глухий кут senior-кандидатів, які впевнені, що structuredClone впаде на циклах так само, як JSON.

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

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

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

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