← Назад к вопросам

Как полностью скопировать объект содержащий ссылки на этот же объект?

2.0 Middle🔥 112 комментариев
#JavaScript Core

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Глубокое копирование объектов с циклическими ссылками

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

Проблема с наивными подходами

Стандартное глубокое копирование не работает с циклическими ссылками:

// Проблема: бесконечный цикл
const obj = { name: 'test' }
obj.self = obj  // циклическая ссылка

// JSON.parse(JSON.stringify(obj)) — выбросит ошибку
// Error: Converting circular structure to JSON

function deepCopy(obj) {
  if (typeof obj !== 'object') return obj
  
  const copy = Array.isArray(obj) ? [] : {}
  for (const key in obj) {
    copy[key] = deepCopy(obj[key])  // бесконечный цикл!
  }
  return copy
}

deepCopy(obj)  // Stack overflow!

Решение 1: WeakMap для отслеживания

Самый эффективный способ — использовать WeakMap для хранения уже скопированных объектов:

function deepCopyWithWeakMap(obj, map = new WeakMap()) {
  // Базовые типы возвращаем как есть
  if (obj === null || typeof obj !== 'object') {
    return obj
  }
  
  // Если объект уже копирован, вернём его копию
  if (map.has(obj)) {
    return map.get(obj)
  }
  
  // Создаём копию (пустую) и добавляем в map ДО рекурсии
  const copy = Array.isArray(obj) ? [] : {}
  map.set(obj, copy)
  
  // Теперь копируем свойства
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepCopyWithWeakMap(obj[key], map)
    }
  }
  
  return copy
}

// Тестирование
const obj = { name: 'test', data: { value: 42 } }
obj.self = obj
obj.data.parent = obj

const copied = deepCopyWithWeakMap(obj)
console.log(copied.name)           // 'test'
console.log(copied.self === copied) // true (циклическая ссылка сохранена)
console.log(copied.data.value)     // 42
console.log(copied.data.parent === copied) // true

Решение 2: Map для более подробной отладки

Если нужна совместимость со старыми браузерами или больше контроля:

function deepCopyWithMap(obj, map = new Map(), depth = 0) {
  if (depth > 1000) throw new Error('Max depth exceeded')
  
  if (obj === null || typeof obj !== 'object') {
    return obj
  }
  
  const objId = getObjectId(obj)
  
  if (map.has(objId)) {
    return map.get(objId)
  }
  
  const copy = Array.isArray(obj) ? [] : {}
  map.set(objId, copy)
  
  for (const key of Object.keys(obj)) {
    copy[key] = deepCopyWithMap(obj[key], map, depth + 1)
  }
  
  return copy
}

let objectCounter = 0
const objectIds = new WeakMap()

function getObjectId(obj) {
  if (!objectIds.has(obj)) {
    objectIds.set(obj, ++objectCounter)
  }
  return objectIds.get(obj)
}

Решение 3: Встроенный structuredClone API

В современных браузерах есть встроенный API:

const obj = { name: 'test' }
obj.self = obj

// Работает из коробки!
const copied = structuredClone(obj)
console.log(copied.self === copied) // true

// Поддерживает сложные типы
const complex = {
  date: new Date(),
  map: new Map([['key', 'value']]),
  self: null
}
complex.self = complex

const copiedComplex = structuredClone(complex)
console.log(copiedComplex.date instanceof Date) // true

ВАЖНО: structuredClone не копирует функции и некоторые встроенные объекты.

Решение 4: С поддержкой специальных типов

Если нужна полная поддержка Date, Map, Set и т.д.:

function deepCopyAdvanced(obj, map = new WeakMap()) {
  if (obj === null) return null
  if (typeof obj !== 'object') return obj
  
  if (map.has(obj)) return map.get(obj)
  
  // Специальные типы
  if (obj instanceof Date) return new Date(obj.getTime())
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)
  if (obj instanceof Map) {
    const newMap = new Map()
    map.set(obj, newMap)
    obj.forEach((v, k) => {
      newMap.set(deepCopyAdvanced(k, map), deepCopyAdvanced(v, map))
    })
    return newMap
  }
  if (obj instanceof Set) {
    const newSet = new Set()
    map.set(obj, newSet)
    obj.forEach(v => newSet.add(deepCopyAdvanced(v, map)))
    return newSet
  }
  
  // Обычные объекты и массивы
  const copy = Array.isArray(obj) ? [] : {}
  map.set(obj, copy)
  
  for (const key of Object.keys(obj)) {
    copy[key] = deepCopyAdvanced(obj[key], map)
  }
  
  return copy
}

Рекомендации

  • WeakMap подход — лучше всего для production кода, минимум памяти
  • structuredClone — используй для современных браузеров, самый простой способ
  • С поддержкой типов — когда работаешь с Date, Map, Set и другими встроенными объектами
  • Избегай JSON методов — они не работают с циклическими ссылками и теряют типы

Выбор зависит от браузеров, которые нужно поддерживать, и типов данных в приложении.