Avatar ·

Как правильно клонировать объект в JavaScript: поверхностное и глубокое копирование

Всем привет! Я только начал изучать JavaScript и столкнулся с проблемой, которая меня поставила в тупик. Когда я присваиваю один объект другому, они почему-то связаны. Изменяя копию, я меняю и оригинал. Вот пример: ```javascript let user = { name: "Иван", age: 30 }; let clone = user; clone.name = "Петр"; console.log(user.name); // Выводит "Петр", а я ожидал "Иван" ``` Я понимаю, что в JavaScript объекты передаются по ссылке, но как мне сделать настоящую независимую копию? Я пробовал `Object.assign()`, но, кажется, он тоже не всегда работает правильно, особенно если внутри объекта есть другие объекты: ```javascript let user = { name: "Анна", address: { city: "Москва", street: "Тверская" } }; let clone = Object.assign({}, user); clone.address.city = "СПб"; console.log(user.address.city); // Выводит "СПб" — опять изменился оригинал! ``` Есть ли способ сделать полное, глубокое копирование? Может быть, есть встроенные методы или нужно писать свою функцию? Слышал про spread-оператор и JSON.parse/JSON.stringify, но не уверен, что это правильно. Подскажите, как безопасно клонировать объект, чтобы копия была полностью независима от оригинала. Заранее спасибо! <<>>

Проблема ссылочного копирования в JavaScript

Вы совершенно правы: в JavaScript объекты (и массивы) хранятся и копируются по ссылке, а не по значению. Когда вы пишете let clone = user, вы не создаёте новый объект — вы создаёте ещё одну переменную, которая указывает на тот же самый объект в памяти. Поэтому изменение через одну переменную сразу видно через другую.

Для создания независимой копии нужно использовать клонирование. Оно бывает двух типов:

  • Поверхностное (shallow copy) — копирует только свойства первого уровня. Вложенные объекты остаются общими.
  • Глубокое (deep copy) — создаёт полностью независимую копию со всеми уровнями вложенности.

1. Поверхностное копирование

Эти методы подходят, если объект не содержит вложенных объектов (только примитивы: строки, числа, булевы значения, null, undefined).

Способ A: Object.assign()

const original = { a: 1, b: 2 };
const copy = Object.assign({}, original);
copy.a = 99;
console.log(original.a); // 1 — оригинал не изменился

Способ B: Spread-оператор (...)

const original = { a: 1, b: 2 };
const copy = { ...original };
copy.a = 99;
console.log(original.a); // 1

Оба способа работают идентично для плоских объектов. Но для вложенных они не спасают.

2. Глубокое копирование

Способ A: JSON.parse + JSON.stringify (простой, но с ограничениями)

Это самый популярный «быстрый» метод для глубокого копирования:

const original = { 
    name: "Анна", 
    address: { city: "Москва" },
    skills: ["JS", "Python"]
};
const deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.address.city = "СПб";
deepCopy.skills.push("Go");
console.log(original.address.city); // "Москва" — не изменилось
console.log(original.skills);       // ["JS", "Python"] — не изменилось

Важные ограничения этого метода:

  • Не копирует функции, undefined, Symbol — они будут потеряны или заменены на null.
  • Не работает с циклическими ссылками (объект ссылается сам на себя) — вызовет ошибку.
  • Не сохраняет специальные объекты: Date станет строкой, Map, Set, RegExp — потеряют свой тип.

Способ B: structuredClone() (современный и правильный)

Начиная с ES2022 (современные браузеры и Node.js 17+), есть встроенная функция structuredClone(). Она решает почти все проблемы:

const original = {
    name: "Анна",
    date: new Date(),
    nested: { arr: [1, 2, { x: 10 }] },
    undef: undefined,
    func: () => console.log("Привет")
};

const clone = structuredClone(original);

clone.nested.arr[2].x = 999;
console.log(original.nested.arr[2].x); // 10 — не изменилось
console.log(clone.date instanceof Date); // true — дата осталась датой

Что умеет structuredClone:

  • Глубокое копирование любой глубины.
  • Работает с Date, Map, Set, RegExp, ArrayBuffer и другими.
  • Корректно обрабатывает циклические ссылки.
  • Не копирует функции и методы (выбрасывает ошибку, если они есть).

3. Самодельная функция для глубокого копирования (рекурсия)

Если вам нужно копировать и функции, или вы работаете в старой среде, можно написать свою функцию:

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 clonedObj = {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            clonedObj[key] = deepClone(obj[key]);
        }
    }
    return clonedObj;
}

// Пример:
const original = { a: 1, b: { c: 2 }, greet: () => "Привет" };
const copy = deepClone(original);
console.log(copy.greet()); // "Привет" — функция скопировалась

Итоговая рекомендация:

  • Для простых объектов без вложений — используйте { ...obj } или Object.assign({}, obj).
  • Для глубокого копирования в современном коде — structuredClone(obj).
  • Если нужна поддержка функций или старых браузеров — напишите рекурсивную функцию или используйте библиотеку (например, Lodash с методом _.cloneDeep()).
<<>>

Альтернативный подход: библиотека Lodash

Если вы не хотите писать свою реализацию или использовать JSON.parse/JSON.stringify с его ограничениями, отличным решением будет популярная библиотека Lodash. Она предоставляет готовый метод _.cloneDeep(), который делает глубокое копирование правильно и без «сюрпризов».

Установите Lodash через npm (или подключите CDN) и используйте:

// npm install lodash
const _ = require('lodash');

const original = {
    name: "Ольга",
    address: { city: "Казань" },
    tags: new Set(["js", "node"]),
    actions: [() => console.log("action")]
};

const clone = _.cloneDeep(original);

clone.address.city = "Уфа";
console.log(original.address.city); // "Казань"

// Lodash корректно копирует Set, Map, Date, функции и даже циклические ссылки

Преимущества Lodash:

  • Проверенное годами решение — используется в миллионах проектов.
  • Работает с функциями, циклическими ссылками, специальными объектами.
  • Не требует писать рекурсивный код вручную.
  • Есть и поверхностный вариант: _.clone().

Недостаток: нужно подключать библиотеку (даже если вам нужен только один метод). Но современные сборщики (Webpack, Vite) позволяют импортировать только нужную функцию, чтобы не раздувать размер бандла:

import cloneDeep from 'lodash/cloneDeep'; // импорт только одной функции

Таким образом, если вы работаете в проекте, где уже используется Lodash, или вам нужно надёжное решение «из коробки» — это отличный выбор.