Клонирование объектов в JavaScript: как не сломать код

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему простое присваивание — это ловушка?

Представьте, что вы пишете приложение для заметок. Пользователь создаёт карточку с заголовком и текстом, потом хочет сделать её копию, чтобы немного изменить. Вы пишете:

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 с редактором уроков. Пользователь редактирует шаблон урока, и вы хотите сохранить исход

Блоги

Рекомендаций по книгам