Введение: почему простое присваивание — это ловушка?
Представьте, что вы пишете приложение для заметок. Пользователь создаёт карточку с заголовком и текстом, потом хочет сделать её копию, чтобы немного изменить. Вы пишете:
const original = { title: 'Мой план', text: 'Купить молоко' };
const copy = original;
copy.title = 'Новый план';
console.log(original.title); // 'Новый план' — ой!
Знакомая ситуация? Если да, то вы столкнулись с главной особенностью объектов в JavaScript: они передаются по ссылке. Присваивание copy = original не создаёт новый объект, а лишь копирует ссылку на тот же участок памяти. Любое изменение в copy отражается на original.
На Stack Overflow вопрос "How do I correctly clone a JavaScript object?" собрал тысячи просмотров и десятки ответов. Это одна из самых частых проблем у новичков и даже опытных разработчиков. В этой статье мы разберём все способы клонирования объектов в JavaScript, от простых до продвинутых, и выясним, какой подход лучше в каждой ситуации.
Мелкое копирование (Shallow copy): быстро и просто
Мелкое копирование создаёт новый объект, но для вложенных объектов (массивов, других объектов) копирует только ссылки. Это идеально для плоских структур, но опасно для глубоких.
Способ 1: Spread-оператор (…)
Самый современный и читаемый способ:
const original = { name: 'Анна', age: 25 };
const clone = { ...original };
clone.age = 30;
console.log(original.age); // 25 — всё в порядке!
Плюсы: короткая запись, работает с массивами и объектами. Минусы: не копирует методы (функции) и свойства-символы.
Способ 2: Object.assign()
Классический метод для копирования объекта без ссылок на верхнем уровне:
const source = { a: 1, b: { c: 2 } };
const target = Object.assign({}, source);
target.a = 100; // source.a не изменится
target.b.c = 999; // source.b.c тоже изменится — shallow!
Этот метод особенно полезен, когда нужно объединить несколько объектов: Object.assign({}, obj1, obj2).
Способ 3: Цикл for…in
Древний, но всё ещё рабочий способ. Полезен для понимания механики:
function shallowClone(obj) {
const clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = obj[key];
}
}
return clone;
}
Сегодня его используют редко, но он отлично демонстрирует, как работает клонирование объектов вручную.
Глубокое копирование (Deep copy): когда нужно всё
Если ваш объект содержит вложенные объекты, массивы или даты, shallow copy вас подведёт. Нужно глубокое клонирование js.
Способ 4: JSON.parse + JSON.stringify
Самый популярный «костыль»:
const original = {
name: 'Петр',
address: { city: 'Москва', street: 'Тверская' },
date: new Date()
};
const clone = JSON.parse(JSON.stringify(original));
clone.address.city = 'СПб';
console.log(original.address.city); // 'Москва' — ура!
Но есть нюансы: этот метод теряет функции, undefined, Symbol, а Date превращается в строку. Для простых данных — ок, для сложных — нет.
Способ 5: structuredClone() — новый стандарт
С 2022 года в JavaScript появился встроенный метод structuredClone:
const original = {
scores: [10, 20, 30],
meta: { level: 'easy' },
date: new Date()
};
const clone = structuredClone(original);
clone.meta.level = 'hard';
console.log(original.meta.level); // 'easy'
Плюсы: поддерживает Date, Map, Set, ArrayBuffer, рекурсивные ссылки. Минусы: не поддерживает функции, DOM-элементы, Error. Работает во всех современных браузерах и Node.js 17+.
Способ 6: Рекурсивная функция (сделай сам)
Для полного контроля напишем свою функцию глубокого копирования объекта:
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof Array) return obj.map(item => deepClone(item));
const clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
Этот код обрабатывает даты, массивы и рекурсивные объекты. Можно расширить для Map, Set и других типов.
Сравнение производительности и области применения
Давайте сведём все способы клонирования объектов в JavaScript в одну таблицу (в мысленном формате):
- Spread / Object.assign — скорость высокая, подходит для плоских объектов без вложенности.
- JSON.parse/stringify — медленнее, но просто. Не подходит для функций и специальных типов.
- structuredClone — оптимальный баланс скорости и глубины. Рекомендуется для 90% случаев.
- Рекурсивная функция — максимальная гибкость, но ниже скорость и больше кода.
Для простых данных (конфиги, ответы API) используйте structuredClone. Для огромных объектов с тысячами полей — возможно, стоит задуматься об иммутабельности и неизменяемых структурах данных (Immer, Immutable.js).
Подводные камни: что нельзя клонировать?
Даже structuredClone не всемогущ. Вот что он не умеет:
- Функции и методы — они просто отбрасываются. Если вам нужно копировать поведение, используйте фабрики или прототипы.
- DOM-элементы — выбросят ошибку. Для них есть
cloneNode(). - Error, WeakMap, WeakSet — не поддерживаются.
- Свойства-геттеры/сеттеры — копируются как значения, а не как функции.
Если ваш объект содержит такие элементы, придётся писать кастомный клонер или использовать библиотеку вроде lodash (_.cloneDeep).
Практический пример: клонирование в реальном проекте
Допустим, вы делаете образовательный сайт на JavaScript с редактором уроков. Пользователь редактирует шаблон урока, и вы хотите сохранить исход