Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як скопіювати об'єкт у JavaScript?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Копіювання об'єкта в JavaScript** створює новий об'єкт з наявних даних. Поверхнева копія (`{...obj}`) дублює тільки верхній рівень - вкладені об'єкти залишаються спільними посиланнями. Глибока копія (`structuredClone`) клонує кожен рівень незалежно. ```javascript const shallow = { ...original }; // вкладені посилання спільні const deep = structuredClone(original); // повністю незалежна копія const json = JSON.parse(JSON.stringify(original)); // глибока, але губить функції і Date ``` **Головне:** мутація вкладеної властивості через поверхневу копію змінює і оригінал.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Копіювання об'єкта в 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 або мовчки дають неправильний результат, або кидають виняток.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.