Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Поверхневе копіювання проти глибокого копіювання в JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Поверхневе копіювання (shallow copy)** копіює властивості верхнього рівня, але вкладені об'єкти залишаються спільними. **Глибоке копіювання (deep copy)** клонує всю структуру незалежно на кожному рівні. ```javascript const obj = { a: 1, nested: { b: 2 } }; const shallow = { ...obj }; // nested.b - спільний const deep = structuredClone(obj); // повністю незалежний shallow.nested.b = 99; console.log(obj.nested.b); // 99 - оригінал змінився ``` **Ключове:** для плоских даних - spread (`{ ...obj }`), для вкладених - `structuredClone()`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Поверхневе копіювання (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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.