Иммутабельность и функции

Иммутабельность — подход, при котором данные не изменяются после создания: вместо мутации исходного объекта/массива создаётся новый с нужными изменениями.

Зачем нужно

Мутация данных — частый источник трудноуловимых багов: функция изменила переданный объект, другая часть кода получила неожиданный результат. Иммутабельный подход делает поток данных предсказуемым, упрощает отладку, позволяет сравнивать состояния по ссылке (===) и является основой реактивных систем (React, Redux).

Где используется

  • Функции без побочных эффектов (pure functions)
  • Redux/состояние в React — reducer-паттерн
  • Обновление вложенных объектов без мутации
  • Массивы: создание новых вместо изменения существующих

Чистые функции (Pure Functions)

// Нечистая: мутирует аргумент
function addItem(cart, item) {
  cart.items.push(item); // изменяет переданный объект!
  return cart;
}

// Чистая: возвращает новый объект
function addItem(cart, item) {
  return {
    ...cart,
    items: [...cart.items, item],
  };
}

Иммутабельная работа с объектами

const user = { name: 'Иван', age: 30, address: { city: 'Москва' } };

// Изменить поверхностное свойство
const updated = { ...user, age: 31 };
console.log(user.age);    // 30 — оригинал не изменён
console.log(updated.age); // 31

// Изменить вложенное свойство (deep update)
const movedUser = {
  ...user,
  address: { ...user.address, city: 'Питер' },
};
console.log(user.address.city);    // 'Москва'
console.log(movedUser.address.city); // 'Питер'

Иммутабельная работа с массивами

const arr = [1, 2, 3, 4, 5];

// Добавить элемент (вместо push)
const withNew = [...arr, 6];

// Удалить элемент (вместо splice)
const without3 = arr.filter(n => n !== 3);

// Изменить элемент по индексу
const withUpdated = arr.map((n, i) => i === 1 ? 99 : n);

// Вставить в середину
const withInserted = [...arr.slice(0, 2), 99, ...arr.slice(2)];

console.log(arr); // [1, 2, 3, 4, 5] — оригинал не изменён

Object.freeze для глубокой иммутабельности

const config = Object.freeze({
  host: 'localhost',
  port: 3000,
  db: Object.freeze({ name: 'mydb' }), // нужно замораживать вложенное тоже
});

config.port = 8080;      // молча игнорируется (TypeError в strict mode)
config.db.name = 'other'; // тоже не изменится — заморожено

console.log(config.port); // 3000

Reducer-паттерн

// Redux-стиль: (state, action) => newState
function cartReducer(state = { items: , total: 0 }, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload],
        total: state.total + action.payload.price,
      };
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload),
      };
    default:
      return state;
  }
}

Частые ошибки

  • Поверхностная копия объекта не защищает вложенные данные{ ...obj } копирует только первый уровень; вложенные объекты остаются общими ссылками.
  • Object.freeze не рекурсивна — замораживает только сам объект, не его вложенные свойства.
  • Перфоманс-опасения — создание копий через spread имеет O(n) стоимость, но для типичных объектов состояния это незначительно.

Связанные темы

Ресурсы