Поверхневе копіювання проти глибокого копіювання в JavaScript
Поверхневе копіювання (shallow copy) створює новий об'єкт із копіями властивостей верхнього рівня, але вкладені об'єкти залишаються спільними. Глибоке копіювання (deep copy) клонує всю структуру рекурсивно - оригінал і копія не мають спільних посилань.
Теорія
Коротко
- Поверхнева копія - це як ксерокопія листа з доданими фото: лист новий, але обидва вказують на ті самі фото
- Глибока копія копіює і фото теж - нічого спільного
{ ...obj }іObject.assign()- поверхневі;structuredClone()іlodash.cloneDeep()- глибокі- Плоский об'єкт без вкладень? Поверхневе копіювання достатньо. Вкладений стан або відповіді API? Потрібне глибоке.
structuredClone()- сучасний стандарт для глибокого копіювання, доступний у всіх браузерах з 2022 року та Node.js 17+
Швидкий приклад
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. Припущення, що спред копіює вкладені об'єкти
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 або функціями
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() обробляє вкладені масиви
const arr = [1, [2, 3]];
const copy = arr.slice(); // поверхневе - вкладені масиви спільні
copy[1][0] = 99;
console.log(arr[1][0]); // 99 - оригінал змінено.slice(), спред і Array.from - усі три роблять поверхневу копію масиву. Вкладені масиви жоден з них не торкається.
4. Циклічні посилання ламають JSON
const obj = { name: 'test' };
obj.self = obj;
JSON.parse(JSON.stringify(obj)); // Throws: Converting circular structure to JSON
structuredClone(obj); // Працює нормальноЯкщо структура даних може містити самопосилання, structuredClone або lodash.cloneDeep - єдині надійні варіанти.
5. Неперелічувані властивості зникають при спреді
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
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
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)
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.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.