← JavaScript/Копирование объектов и ссылки#66 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Копирование объектов и ссылки

Реальная проблема: настройки пользователя

В Notion вы открываете настройки, меняете тему с тёмной на светлую — и вдруг тема меняется для всех пользователей сразу. Это именно та ошибка, которую делают новички: изменяют «копию» объекта, а на самом деле изменяют оригинал. Потому что объекты в JavaScript передаются по ссылке.

Что такое ссылка

Когда вы присваиваете объект переменной, переменная хранит не сам объект, а адрес в памяти — ссылку на него. При присваивании копируется ссылка, а не данные.

На основе предыдущих уроков

  • «Типы данных» — примитивы vs объекты, разница в хранении
  • «Объекты» — синтаксис объектов, свойства
  • «Преобразование типов» — JSON.stringify/parse для глубокого копирования
  • Примитивы vs Объекты

    // Примитивы — копируются по значению
    let a = 5
    let b = a
    b = 10
    console.log(a)  // 5 — a не изменилась
    
    // Объекты — копируются по ссылке
    let settings1 = { theme: 'dark', lang: 'ru' }
    let settings2 = settings1     // settings2 указывает на ТОТ ЖЕ объект
    settings2.theme = 'light'
    console.log(settings1.theme)  // 'light' — изменился оригинал!

    Поверхностное копирование (shallow copy)

    Создаёт новый объект с копией свойств первого уровня:

    const original = { name: 'Алексей', age: 28 }
    
    // Способ 1: spread-оператор (предпочтительный)
    const copy1 = { ...original }
    
    // Способ 2: Object.assign
    const copy2 = Object.assign({}, original)
    
    copy1.name = 'Михаил'
    console.log(original.name)  // 'Алексей' — не изменился

    Проблема поверхностного копирования

    Вложенные объекты всё равно передаются по ссылке:

    const user = {
      name: 'Алексей',
      address: { city: 'Москва', zip: '101000' }
    }
    
    const copy = { ...user }
    copy.name = 'Иван'            // OK — не затрагивает original
    copy.address.city = 'СПб'    // МУТАЦИЯ! address — всё ещё та же ссылка
    
    console.log(user.name)          // 'Алексей' — не изменилось
    console.log(user.address.city)  // 'СПб' — изменилось!

    Глубокое копирование (deep copy)

    Способ 1: JSON — простой, но теряет функции, Date, undefined:

    const deepCopy = JSON.parse(JSON.stringify(user))
    deepCopy.address.city = 'Казань'
    console.log(user.address.city)  // 'СПб' — не изменился

    Способ 2: structuredClone — современный стандарт, сохраняет Date и Map:

    const deepCopy = structuredClone(user)

    Способ 3: ручное копирование через spread — когда нужен контроль:

    const deepCopy = {
      ...user,
      address: { ...user.address }
    }

    Типичные ошибки

    Ошибка 1: мутация в функции

    // Сломано: функция меняет исходный объект
    function applyDiscount(product, percent) {
      product.price = product.price * (1 - percent / 100)  // МУТАЦИЯ!
      return product
    }
    
    const laptop = { name: 'Ноутбук', price: 80000 }
    const sale   = applyDiscount(laptop, 10)
    console.log(laptop.price)  // 72000 — исходный изменился!
    
    // Исправлено: возвращаем новый объект
    function applyDiscount(product, percent) {
      return { ...product, price: product.price * (1 - percent / 100) }
    }

    Ошибка 2: поверхностная копия при вложенных данных

    // Сломано:
    const newUser = { ...user }
    newUser.settings.theme = 'light'  // изменит и user.settings.theme!
    
    // Исправлено:
    const newUser = { ...user, settings: { ...user.settings, theme: 'light' } }

    Ошибка 3: сравнение объектов через ===

    // Сломано: сравниваются ссылки, а не содержимое
    const a = { x: 1 }
    const b = { x: 1 }
    console.log(a === b)  // false — разные объекты в памяти!
    
    // Для сравнения содержимого:
    console.log(JSON.stringify(a) === JSON.stringify(b))  // true

    В реальных проектах

  • React/Redux: никогда не мутируйте state — всегда возвращайте новый объект через spread
  • Настройки: { ...defaultSettings, ...userSettings } — безопасное слияние
  • История изменений: при сохранении версии документа нужно глубокое копирование
  • Immutable.js и Immer — библиотеки для удобной работы с неизменяемыми данными
  • Примеры

    Система настроек Notion-приложения без мутаций

    // Настройки по умолчанию
    const defaultSettings = {
      theme: 'dark',
      fontSize: 14,
      editor: {
        spellcheck: true,
        lineNumbers: false,
        tabSize: 2,
      }
    }
    
    // Обновление настройки первого уровня — spread достаточно
    function setTheme(settings, theme) {
      return { ...settings, theme }
    }
    
    // Обновление вложенного свойства — нужен вложенный spread
    function setEditorOption(settings, key, value) {
      return {
        ...settings,
        editor: { ...settings.editor, [key]: value }
      }
    }
    
    const userSettings = setTheme(defaultSettings, 'light')
    console.log(defaultSettings.theme)  // 'dark' — не изменился
    console.log(userSettings.theme)     // 'light'
    
    const finalSettings = setEditorOption(userSettings, 'lineNumbers', true)
    console.log(userSettings.editor.lineNumbers)  // false — не изменился
    console.log(finalSettings.editor.lineNumbers) // true
    
    // Глубокая копия через structuredClone
    const saved = structuredClone(finalSettings)
    saved.editor.tabSize = 4
    console.log(finalSettings.editor.tabSize)  // 2 — не изменился

    Копирование объектов и ссылки

    Реальная проблема: настройки пользователя

    В Notion вы открываете настройки, меняете тему с тёмной на светлую — и вдруг тема меняется для всех пользователей сразу. Это именно та ошибка, которую делают новички: изменяют «копию» объекта, а на самом деле изменяют оригинал. Потому что объекты в JavaScript передаются по ссылке.

    Что такое ссылка

    Когда вы присваиваете объект переменной, переменная хранит не сам объект, а адрес в памяти — ссылку на него. При присваивании копируется ссылка, а не данные.

    На основе предыдущих уроков

  • «Типы данных» — примитивы vs объекты, разница в хранении
  • «Объекты» — синтаксис объектов, свойства
  • «Преобразование типов» — JSON.stringify/parse для глубокого копирования
  • Примитивы vs Объекты

    // Примитивы — копируются по значению
    let a = 5
    let b = a
    b = 10
    console.log(a)  // 5 — a не изменилась
    
    // Объекты — копируются по ссылке
    let settings1 = { theme: 'dark', lang: 'ru' }
    let settings2 = settings1     // settings2 указывает на ТОТ ЖЕ объект
    settings2.theme = 'light'
    console.log(settings1.theme)  // 'light' — изменился оригинал!

    Поверхностное копирование (shallow copy)

    Создаёт новый объект с копией свойств первого уровня:

    const original = { name: 'Алексей', age: 28 }
    
    // Способ 1: spread-оператор (предпочтительный)
    const copy1 = { ...original }
    
    // Способ 2: Object.assign
    const copy2 = Object.assign({}, original)
    
    copy1.name = 'Михаил'
    console.log(original.name)  // 'Алексей' — не изменился

    Проблема поверхностного копирования

    Вложенные объекты всё равно передаются по ссылке:

    const user = {
      name: 'Алексей',
      address: { city: 'Москва', zip: '101000' }
    }
    
    const copy = { ...user }
    copy.name = 'Иван'            // OK — не затрагивает original
    copy.address.city = 'СПб'    // МУТАЦИЯ! address — всё ещё та же ссылка
    
    console.log(user.name)          // 'Алексей' — не изменилось
    console.log(user.address.city)  // 'СПб' — изменилось!

    Глубокое копирование (deep copy)

    Способ 1: JSON — простой, но теряет функции, Date, undefined:

    const deepCopy = JSON.parse(JSON.stringify(user))
    deepCopy.address.city = 'Казань'
    console.log(user.address.city)  // 'СПб' — не изменился

    Способ 2: structuredClone — современный стандарт, сохраняет Date и Map:

    const deepCopy = structuredClone(user)

    Способ 3: ручное копирование через spread — когда нужен контроль:

    const deepCopy = {
      ...user,
      address: { ...user.address }
    }

    Типичные ошибки

    Ошибка 1: мутация в функции

    // Сломано: функция меняет исходный объект
    function applyDiscount(product, percent) {
      product.price = product.price * (1 - percent / 100)  // МУТАЦИЯ!
      return product
    }
    
    const laptop = { name: 'Ноутбук', price: 80000 }
    const sale   = applyDiscount(laptop, 10)
    console.log(laptop.price)  // 72000 — исходный изменился!
    
    // Исправлено: возвращаем новый объект
    function applyDiscount(product, percent) {
      return { ...product, price: product.price * (1 - percent / 100) }
    }

    Ошибка 2: поверхностная копия при вложенных данных

    // Сломано:
    const newUser = { ...user }
    newUser.settings.theme = 'light'  // изменит и user.settings.theme!
    
    // Исправлено:
    const newUser = { ...user, settings: { ...user.settings, theme: 'light' } }

    Ошибка 3: сравнение объектов через ===

    // Сломано: сравниваются ссылки, а не содержимое
    const a = { x: 1 }
    const b = { x: 1 }
    console.log(a === b)  // false — разные объекты в памяти!
    
    // Для сравнения содержимого:
    console.log(JSON.stringify(a) === JSON.stringify(b))  // true

    В реальных проектах

  • React/Redux: никогда не мутируйте state — всегда возвращайте новый объект через spread
  • Настройки: { ...defaultSettings, ...userSettings } — безопасное слияние
  • История изменений: при сохранении версии документа нужно глубокое копирование
  • Immutable.js и Immer — библиотеки для удобной работы с неизменяемыми данными
  • Примеры

    Система настроек Notion-приложения без мутаций

    // Настройки по умолчанию
    const defaultSettings = {
      theme: 'dark',
      fontSize: 14,
      editor: {
        spellcheck: true,
        lineNumbers: false,
        tabSize: 2,
      }
    }
    
    // Обновление настройки первого уровня — spread достаточно
    function setTheme(settings, theme) {
      return { ...settings, theme }
    }
    
    // Обновление вложенного свойства — нужен вложенный spread
    function setEditorOption(settings, key, value) {
      return {
        ...settings,
        editor: { ...settings.editor, [key]: value }
      }
    }
    
    const userSettings = setTheme(defaultSettings, 'light')
    console.log(defaultSettings.theme)  // 'dark' — не изменился
    console.log(userSettings.theme)     // 'light'
    
    const finalSettings = setEditorOption(userSettings, 'lineNumbers', true)
    console.log(userSettings.editor.lineNumbers)  // false — не изменился
    console.log(finalSettings.editor.lineNumbers) // true
    
    // Глубокая копия через structuredClone
    const saved = structuredClone(finalSettings)
    saved.editor.tabSize = 4
    console.log(finalSettings.editor.tabSize)  // 2 — не изменился

    Задание

    В корзине интернет-магазина есть функция updateQuantity(cart, productId, qty). Напиши её так, чтобы она возвращала новый объект корзины с обновлённым количеством нужного товара, НЕ изменяя исходную корзину. Оригинальный cart должен остаться нетронутым.

    Подсказка

    Используй spread для корзины и map для массива items: items: cart.items.map(item => item.id === productId ? { ...item, qty } : item)

    Загружаем среду выполнения...
    Загружаем AI-помощника...