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

Как достигается иммутабельность в JavaScript?

2.2 Middle🔥 161 комментариев
#JavaScript Core

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

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

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

Иммутабельность в JavaScript

Иммутабельность (immutability) — это принцип, когда значение, однажды созданное, не может быть изменено. Вместо изменения существующего значения создаётся новое значение. Это фундаментальный концепт в функциональном программировании и критичен для React.

Почему иммутабельность важна?

1. Предсказуемость

Если значение не меняется, код проще понять:

// Мутабельно (неправильно)
const user = { name: 'Alice', age: 30 };
function increaseAge(u) {
  u.age = u.age + 1; // меняем оригинальный объект
  return u;
}

const userAfter = increaseAge(user);
console.log(user.age); // 31 — оригинальный объект изменился!
console.log(userAfter.age); // 31
// Это очень запутанно — неясно, что именно меняется

// Иммутабельно (правильно)
function increaseAge(u) {
  return { ...u, age: u.age + 1 }; // создаём новый объект
}

const userAfter = increaseAge(user);
console.log(user.age); // 30 — оригинальный не изменился
console.log(userAfter.age); // 31
// Ясно, что создан новый объект

2. Обнаружение изменений в React

// React полагается на иммутабельность для оптимизации
function UserComponent() {
  const [user, setUser] = useState({ name: 'Alice' });
  const [count, setCount] = useState(0);

  // ПЛОХО: мутабельное обновление
  const handleBad = () => {
    user.name = 'Bob'; // меняем прямо
    setUser(user); // React может не заметить изменение!
    // React сравнивает ссылки: user === user (true)
    // React не знает, что объект изменился
  };

  // ХОРОШО: иммутабельное обновление
  const handleGood = () => {
    setUser({ ...user, name: 'Bob' }); // новый объект
    // React сравнивает ссылки: newUser !== oldUser (true)
    // React видит изменение и перерисовывает
  };

  return <div>User: {user.name}</div>;
}

Техники достижения иммутабельности

1. Spread operator для объектов

// Создание копии объекта
const original = { name: 'Alice', age: 30, email: 'alice@example.com' };

// Способ 1: Spread operator
const updated = { ...original, age: 31 };
// { name: 'Alice', age: 31, email: 'alice@example.com' }

// Способ 2: Object.assign
const updated2 = Object.assign({}, original, { age: 31 });
// { name: 'Alice', age: 31, email: 'alice@example.com' }

// Способ 3: structuredClone (глубокая копия)
const cloned = structuredClone(original);

2. Spread operator для массивов

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

// Добавить элемент
const withNew = [...original, 6];
// [1, 2, 3, 4, 5, 6]

// Обновить элемент по индексу
const updated = original.map((num, i) => (i === 2 ? 99 : num));
// [1, 2, 99, 4, 5]

// Удалить элемент
const removed = original.filter((num, i) => i !== 2);
// [1, 2, 4, 5]

// Заменить диапазон
const replaced = [
  ...original.slice(0, 1),  // [1]
  10, 11, 12,               // новые элементы
  ...original.slice(3)      // [4, 5]
];
// [1, 10, 11, 12, 4, 5]

3. Методы для иммутабельности

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

// map, filter, reduce — иммутабельны
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]

const evens = numbers.filter(n => n % 2 === 0);
// [2, 4]

const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15

// push, pop, shift, unshift — мутабельны (ИЗБЕГАЙ!)
// concat, slice — иммутабельны
const added = numbers.concat([6, 7]);
// [1, 2, 3, 4, 5, 6, 7]

const sliced = numbers.slice(1, 4);
// [2, 3, 4]

4. Глубокая копия вложенных объектов

const user = {
  name: 'Alice',
  address: {
    city: 'New York',
    zip: '10001'
  },
  hobbies: ['reading', 'coding']
};

// ПЛОХО: поверхностная копия (shallow copy)
const shallow = { ...user };
shallow.address.city = 'Boston';
console.log(user.address.city); // 'Boston' — оригинальный изменился!

// ХОРОШО: глубокая копия (deep copy)
const deep = {
  ...user,
  address: { ...user.address, city: 'Boston' },
  hobbies: [...user.hobbies]
};
console.log(user.address.city); // 'New York' — оригинальный не изменился

// ИЛИ structuredClone для полной копии
const cloned = structuredClone(user);
cloned.address.city = 'Boston';
console.log(user.address.city); // 'New York' — не изменился

5. Обновление вложенных структур

const state = {
  user: {
    id: 1,
    name: 'Alice',
    profile: {
      bio: 'Developer',
      avatar: 'url'
    }
  },
  posts: [
    { id: 1, title: 'Post 1' },
    { id: 2, title: 'Post 2' }
  ]
};

// Обновить user.profile.bio
const updated1 = {
  ...state,
  user: {
    ...state.user,
    profile: {
      ...state.user.profile,
      bio: 'Senior Developer'
    }
  }
};

// Обновить второй post
const updated2 = {
  ...state,
  posts: state.posts.map((post, i) =>
    i === 1 ? { ...post, title: 'Updated Post 2' } : post
  )
};

Иммутабельность в React

1. useState с объектами

function UserForm() {
  const [user, setUser] = useState({ name: '', email: '' });

  // ПЛОХО
  const handleBadChange = (field, value) => {
    user[field] = value; // мутация!
    setUser(user);
  };

  // ХОРОШО
  const handleGoodChange = (field, value) => {
    setUser({
      ...user,
      [field]: value
    });
  };

  // ИЛИ лучше — отдельный стейт для каждого поля
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  return (
    <form>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
    </form>
  );
}

2. useReducer с иммутабельностью

const initialState = {
  users: [],
  loading: false,
  error: null
};

function reducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true };

    case 'FETCH_SUCCESS':
      return {
        ...state,
        users: action.payload,
        loading: false,
        error: null
      };

    case 'FETCH_ERROR':
      return {
        ...state,
        loading: false,
        error: action.payload
      };

    case 'ADD_USER':
      return {
        ...state,
        users: [...state.users, action.payload]
      };

    case 'UPDATE_USER':
      return {
        ...state,
        users: state.users.map(user =>
          user.id === action.payload.id
            ? { ...user, ...action.payload.changes }
            : user
        )
      };

    case 'DELETE_USER':
      return {
        ...state,
        users: state.users.filter(u => u.id !== action.payload)
      };

    default:
      return state;
  }
}

function UserManager() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      {state.users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

3. Immer для упрощения иммутабельности

import produce from 'immer';

const state = {
  user: { name: 'Alice', age: 30 },
  posts: [{ id: 1, title: 'Post 1' }]
};

// С Immer можно писать мутабельный код, он будет иммутабельным
const updated = produce(state, (draft) => {
  draft.user.name = 'Bob'; // выглядит как мутация
  draft.posts.push({ id: 2, title: 'Post 2' }); // выглядит как мутация
  // Но Immer создаст новый объект автоматически
});

console.log(state.user.name); // 'Alice' — оригинал не изменился
console.log(updated.user.name); // 'Bob'

// В React
const [state, setState] = useState(initialState);

const updateName = (newName) => {
  setState(
    produce((draft) => {
      draft.user.name = newName;
    })
  );
};

Лучшие практики

1. Используй правильные методы

// ✓ Иммутабельные методы массивов
.map(), .filter(), .reduce(), .concat(), .slice()

// ✗ Мутабельные методы (ИЗБЕГАЙ)
.push(), .pop(), .shift(), .unshift(), .splice(), .sort(), .reverse()

2. Структура стейта

// ПЛОХО: глубоко вложенная структура
const state = {
  app: {
    ui: {
      modal: {
        isOpen: true,
        type: 'edit'
      }
    }
  }
};

// ХОРОШО: плоская структура
const state = {
  isModalOpen: true,
  modalType: 'edit'
};

// Если нужна вложенность, используй Immer

3. Селекторы для сложного стейта

// С Redux Toolkit или Zustand
const selectUserName = (state) => state.user.name;
const selectUserEmail = (state) => state.user.email;
const selectActivePostIds = (state) =>
  state.posts.filter(p => p.active).map(p => p.id);

// Использование
const name = useSelector(selectUserName);

4. Memoization для оптимизации

import { useMemo } from 'react';

function ExpensiveComponent({ items }) {
  // Пересчитывается только если items изменился
  const filtered = useMemo(
    () => items.filter(item => item.active),
    [items]
  );

  return <div>{filtered.length}</div>;
}

Сравнение подходов

const original = {
  items: [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ]
};

// Добавить элемент

// 1. Spread + concat
const v1 = {
  ...original,
  items: original.items.concat({ id: 3, name: 'Item 3' })
};

// 2. Spread + spread
const v2 = {
  ...original,
  items: [...original.items, { id: 3, name: 'Item 3' }]
};

// 3. Immer
const v3 = produce(original, draft => {
  draft.items.push({ id: 3, name: 'Item 3' });
});

// Все результаты идентичны

Производительность

// Shallow comparison (достаточно для большинства случаев)
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1, a: 1 }; // новый объект, но a не изменился
obj1 === obj2; // false (разные ссылки)
obj1.b === obj2.b; // true (вложенный объект не изменился)

// Это оптимизируется с useMemo и useCallback
const memoized = useMemo(() => ({ a: 1, b: { c: 2 } }), []);
// Объект создаётся один раз

Итог

Иммутабельность в JavaScript достигается:

  1. Spread operator — {...obj, field: newValue}
  2. Array методы — map, filter, concat вместо push, pop
  3. Object.assign — создание копии
  4. structuredClone — глубокая копия
  5. Immer — упрощение синтаксиса

Это критично для React, потому что:

  • React полагается на сравнение ссылок для обнаружения изменений
  • Иммутабельность делает код предсказуемым
  • Упрощает отладку и тестирование
  • Позволяет эффективно использовать memoization