Разбор различий между shallow и deep copy в JS. Все способы глубокого копирования: JSON, structuredClone, Lodash и написание собственной рекурсивной функции.
Поверхностное и глубокое копирование в JavaScript
Копирование объектов в JavaScript — это не просто создание дубликата. Это выбор между копированием ссылок (поверхностное) и полным клонированием структуры данных (глубокое). Непонимание этой разницы — источник 90% багов при работе с состоянием в JS.
В JavaScript объекты и массивы являются ссылочными типами данных. Когда вы присваиваете один объект другому (const b = a), вы копируете не сами данные, а ссылку на область памяти. Изменение b всегда затронет a.
Чтобы создать независимую копию, в языке предусмотрены механизмы поверхностного (Shallow) и глубокого (Deep) копирования. Выбор между ними зависит от структуры вашего объекта: является ли он «плоским» или содержит вложенные уровни.
Поверхностное копирование (Shallow Copy)
Поверхностная копия создает новый объект на первом уровне, но все вложенные объекты копируются по ссылке. Для этого используются оператор расширения (...) или метод Object.assign().
const original = {
a: 1,
nested: { b: 2 },
date: new Date()
};
// Создаем поверхностную копию
const shallowCopy = { ...original };
// или: const shallowCopy = Object.assign({}, original);
// 1. Примитивы копируются по значению
shallowCopy.a = 99;
console.log(original.a); // 1 (оригинал не изменился)
// 2. Вложенные объекты копируются по ссылке!
shallowCopy.nested.b = 99;
console.log(original.nested.b); // 99 (оригинал изменен!)
// 3. Объекты Date тоже копируются по ссылке
shallowCopy.date.setFullYear(2000);
console.log(original.date.getFullYear()); // 2000 (оригинал изменен!)
Когда использовать: Поверхностное копирование идеально подходит для плоских объектов (без вложенности) или когда вам намеренно нужно сохранить связь с вложенными структурами. Для многоуровневых данных оно не подходит.
Глубокое копирование (Deep Copy)
Глубокое копирование рекурсивно обходит всю структуру объекта и создает полностью независимые копии всех вложенных элементов. Изменение любой части глубокой копии никогда не затронет оригинал. Рассмотрим все актуальные способы.
1. Классический хак: JSON.parse и JSON.stringify
Долгое время это был единственный встроенный способ сделать глубокую копию. Объект сериализуется в строку, а затем парсится обратно.
const original = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(original));
copy.b.c = 99;
console.log(original.b.c); // 2 (оригинал не изменился)
Ограничения: Метод «ломается» на многих типах данных. Он игнорирует undefined, функции и Symbol. Объекты Date превращаются в строки, RegExp — в пустые объекты, а при наличии циклических ссылок код выбросит ошибку TypeError.
2. Современный нативный способ: structuredClone
Начиная с 2022 года, во всех современных браузерах и Node.js (с версии 17) доступен глобальный метод structuredClone(). Это официальный стандарт для глубокого копирования.
const original = {
date: new Date(),
regex: /test/gi,
nested: { value: 42 }
};
// Даже циклическая ссылка не сломает метод
original.self = original;
const copy = structuredClone(original);
copy.nested.value = 100;
console.log(original.nested.value); // 42
Преимущества: Корректно обрабатывает Date, RegExp, Map, Set, ArrayBuffer и циклические ссылки. Ограничения: Не умеет копировать функции и DOM-узлы.
3. Сторонние библиотеки: Lodash
Если вы поддерживаете легаси-код или работаете в среде, где structuredClone недоступен, стандартом де-факто остается функция _.cloneDeep() из библиотеки Lodash.
import _ from 'lodash';
const copy = _.cloneDeep(original);
Lodash учитывает огромное количество крайних случаев (edge cases). Минус — необходимость тянуть в проект зависимость.
4. Написание собственной функции
На собеседованиях или в специфичных задачах требуется написать функцию глубокого копирования самостоятельно. Главная сложность — не попасть в бесконечную рекурсию из-за циклических ссылок.
Для решения этой проблемы используется WeakMap, который хранит уже скопированные объекты.
function deepClone(obj, hash = new WeakMap()) {
// 1. Базовые случаи: null, примитивы, функции
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 2. Защита от циклических ссылок
if (hash.has(obj)) {
return hash.get(obj);
}
// 3. Обработка специфичных встроенных объектов
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
// 4. Создание пустого клона (массив или объект)
const clone = Array.isArray(obj) ? [] : {};
// 5. Сохраняем ссылку на клон до рекурсии
hash.set(obj, clone);
// 6. Рекурсивное копирование свойств
for (let key in obj) {
if (Object.hasOwn(obj, key)) {
clone[key] = deepClone(obj[key], hash);
}
}
return clone;
}
// Проверка
const testObj = { a: 1, b: { c: 2 }, d: new Date() };
testObj.self = testObj; // Циклическая ссылка
const cloned = deepClone(testObj);
cloned.b.c = 999;
console.log(testObj.b.c); // 2 (оригинал цел)
Сводная таблица методов копирования
| Метод | Вложенные объекты | Date / RegExp | Циклические ссылки | Актуальность |
|---|---|---|---|---|
Spread / Object.assign |
❌ Копирует по ссылке | ❌ Копирует по ссылке | ❌ Игнорирует | Только для плоских (одноуровневых) объектов |
JSON.parse(stringify) |
✅ Глубоко | ❌ Ломает типы | ❌ Вызывает ошибку | Только для простых JSON-данных |
structuredClone() |
✅ Глубоко | ✅ Корректно | ✅ Поддерживает | Стандарт для 99% задач сегодня |
_.cloneDeep() |
✅ Глубоко | ✅ Корректно | ✅ Поддерживает | Легаси или специфичные задачи |
| Своя функция | ✅ Глубоко | ✅ (вручную) | ✅ (с WeakMap) | Собеседования и кастомная логика |
Что запомнить
- Shallow Copy (
...,Object.assign) копирует только первый уровень. Вложенные объекты остаются общими ссылками. - Deep Copy создает полностью независимую структуру данных на всех уровнях вложенности.
JSON.parse(JSON.stringify())— устаревший хак, который ломает функции,Date,RegExpи падает на циклических ссылках.structuredClone()— современный нативный стандарт. Используйте его по умолчанию во всех новых проектах.- Ни один нативный метод не копирует функции и DOM-узлы, так как они не являются данными.
- При написании собственной функции глубокого копирования обязательно используйте
WeakMapдля предотвращения бесконечной рекурсии (циклических ссылок). - Собственная функция должна отдельно обрабатывать
DateиRegExp, так как при обычном рекурсивном обходе они превращаются в пустые объекты{}.