Копирование объектов: поверхностное и глубокое
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Строка const copy = user не создаёт копию — она создаёт второе имя для того же объекта. Разберёмся, как получить настоящую копию и почему «настоящая» бывает двух глубин.
Переменная не хранит объект целиком — она хранит ссылку на него, адрес коробки на складе. Присваивание переписывает адрес в новую переменную, а склад остаётся один. Поэтому две переменные могут смотреть на один и тот же объект и мешать друг другу.
С примитивами такого не бывает: let b = a для числа или строки даёт независимое значение. Объекты, массивы и функции — другое дело, и === для них отвечает не «одинаковое ли содержимое», а «один ли это объект».
Чтобы получить новый объект, его нужно создать явно и переложить в него свойства. Короткий способ — spread-синтаксис { ...obj }: он создаёт свежий объект и копирует в него все свои перечисляемые свойства исходного.
Object.assign(target, ...sources) делает то же самое, но пишет в существующий объект и возвращает его. Он удобен, когда нужно слить несколько источников: свойства правых источников перекрывают левые. Первым аргументом почти всегда стоит пустой {} — иначе вы мутируете чужой объект.
Копия через spread — всегда обычный объект. Прототип не переносится: скопировав экземпляр класса, вы получите набор данных без методов.
Обе конструкции копируют значения свойств. Если значение — число или строка, копия независима. Если значение — объект, то копируется ссылка на него, и вложенный объект оказывается общим у оригинала и копии. Это и называется поверхностным копированием: свежий только верхний этаж.
Такие баги самые неприятные: код выглядит правильно, объект «скопирован», а правка в одном месте всплывает в другом через неделю. Как только в объекте появляется вложенность, нужно осознанно решить — вам хватит поверхностной копии или требуется глубокая.
const замораживает не объект, а привязку имени к ссылке. Менять содержимое можно, нельзя присвоить переменной другой объект.
Глубокая копия — это копия объекта вместе со всем содержимым на всех уровнях. Раньше её приходилось писать руками или тащить lodash, теперь в браузерах и Node есть встроенная функция structuredClone(value). Что ещё из lodash забрала себе платформа и когда маленькая библиотека утилит всё-таки нужна — на полке браузера в части II.
Она умеет то, обо что спотыкаются самодельные рекурсии: правильно копирует Date, Map, Set, массивы и типизированные массивы, а циклические ссылки не зацикливают её, а переносятся в копию как циклы. Единственное серьёзное ограничение — функции и DOM-узлы скопировать нельзя, на них будет ошибка DataCloneError.
Учебная версия на рекурсии: примитивы возвращаем как есть, объекты и массивы обходим по ключам. Она не умеет ни Date, ни циклы — но хорошо показывает саму идею.
Старый народный приём: превратить объект в JSON-строку и разобрать обратно — JSON.parse(JSON.stringify(obj)). Глубокая копия действительно получается, но по дороге объект проходит через формат, который знает только строки, числа, булевы значения, null, массивы и простые объекты. Всё остальное теряется молча.
Плюс на циклической ссылке этот приём просто падает с ошибкой. Вывод простой: для чистых данных с сервера он допустим, во всех остальных случаях берите structuredClone.
| Что в объекте | spread | structuredClone | JSON |
|---|---|---|---|
| Вложенный объект | общий | копируется | копируется |
| Функция | переносится | ошибка | исчезает |
| Date | общая ссылка | настоящая Date | строка |
| Map и Set | общая ссылка | копируются | пустой {} |
| Свойство со значением undefined | остаётся | остаётся | ключ исчезает |
| Циклическая ссылка | не мешает | сохраняется | ошибка |
- Переменная хранит ссылку на объект, поэтому присваивание даёт второе имя, а не копию;
===сравнивает именно идентичность объектов. { ...obj }иObject.assign({}, obj)создают новый объект, но копируют только верхний уровень — вложенные объекты остаются общими.- Такая копия называется поверхностной; она безопасна для плоских данных и опасна, как только появляется вложенность.
structuredClone(obj)делает глубокую копию, переносит Date, Map, Set и циклические ссылки, но падает на функциях и DOM-узлах.JSON.parse(JSON.stringify(obj))тоже копирует вглубь, однако теряет функции, свойства со значением undefined, превращает Date в строку и ломается на циклах.
1/3Настройки слили вызовом Object.assign(defaults, prefs) — без пустого объекта первым аргументом. Что станет с defaults?
2/3Объявлено const user = { sizes: { h: 170 } } и const clone = { ...user }. Что выведет console.log(user.sizes === clone.sizes)?
3/3Что произойдёт при JSON.parse(JSON.stringify(obj)), если у obj есть свойство-метод и свойство типа Date?
Копия, которая ничего не делит с оригиналом
Код собирался сделать копию отчёта и поменять в ней счётчик просмотров, но испортил оригинал. Почините его так, чтобы source.meta.views остался равен 10.
Spread копирует только верхний уровень, поэтому meta у оригинала и копии — один объект. Нужна глубокая копия:
Для такого объекта сработал бы и JSON.parse(JSON.stringify(source)) — здесь только строки и числа. Но стоит добавить в meta дату или метод, и приём начнёт тихо терять данные, поэтому привычкой лучше сделать structuredClone.
Слияние настроек без порчи исходника
Напишите функцию merge(defaults, prefs), которая возвращает новый объект с настройками пользователя поверх умолчаний. Ни defaults, ни prefs меняться не должны.
Раскрываем оба объекта в новый литерал: свойства правого перекрывают левый, а оба источника остаются нетронутыми.
Эквивалент через Object.assign({}, defaults, prefs) — важен именно пустой первый аргумент. Вариант Object.assign(defaults, prefs) испортил бы умолчания. И помните: если внутри настроек появятся вложенные объекты, такое слияние останется поверхностным.