Skip to main content

Що таке імутабельність?

Імутабельність (immutability) - коли ти створив значення, ти його не змінюєш. Замість цього створюєш нове значення з потрібними змінами, а оригінал залишається як є.

Теорія

TL;DR

  • Як фото: оригінал не редагуєш, але можеш зробити нову копію з іншими налаштуваннями
  • Мутація змінює об'єкт за тією ж адресою в пам'яті; імутабельність виділяє нову адресу
  • React і Redux порівнюють посилання на об'єкти, а не їх вміст, тому без імутабельності вони не побачать змін
  • Використовуй, коли кілька частин коду посилаються на один об'єкт, або коли треба передбачувано відстежувати зміни стану

Короткий приклад

javascript
// Мутація - змінює оригінал const user = { name: "Alice", age: 25 }; user.age = 26; console.log(user); // { name: "Alice", age: 26 } - оригінал змінено // Імутабельність - створює новий об'єкт const user2 = { name: "Alice", age: 25 }; const updatedUser = { ...user2, age: 26 }; console.log(user2); // { name: "Alice", age: 25 } - без змін console.log(updatedUser); // { name: "Alice", age: 26 } - новий об'єкт

Spread-оператор виділяє новий об'єкт у пам'яті. Оригінальний user2 залишається за старою адресою, незмінним.

Головна різниця

Коли ти мутуєш об'єкт, ти змінюєш дані за вже існуючою адресою в пам'яті. Всі змінні, які вказують на цю адресу, бачать зміну - навіть якщо ти цього не хотів. З імутабельністю ти не чіпаєш оригінал: новий об'єкт отримує свою адресу, старий залишається незмінним. Це робить відстеження стану набагато передбачуванішим.

Коли застосовувати

  • React-стан: useState очікує нове посилання на об'єкт, щоб запустити ре-рендер; якщо мутувати і передати те саме посилання - нічого не відбудеться
  • Redux-редюсери: завжди повертають новий об'єкт стану, ніколи не змінюють попередній
  • Спільні дані: якщо кілька функцій посилаються на один об'єкт, мутації породжують баги, які важко відстежити
  • Параметри функцій: якщо функція повертає новий масив замість мутації вхідного, вона не має побічних ефектів для викликаючого коду
  • Асинхронний код: коли кілька операцій звертаються до одних і тих самих даних, незмінні значення запобігають гонкам стану

Типові помилки

Помилка 1: Присвоєння - це не копія

javascript
const original = { count: 0 }; const copy = original; // те саме посилання, не копія copy.count = 1; console.log(original.count); // 1 - обидва вказують на один об'єкт // Правильно const safeCopy = { ...original }; safeCopy.count = 1; console.log(original.count); // 0 - оригінал цілий

Помилка 2: Поверхнева копія не захищає вкладені об'єкти

javascript
const user = { name: "Alice", settings: { theme: "dark" } }; const copy = { ...user }; copy.settings.theme = "light"; console.log(user.settings.theme); // "light" - вкладений об'єкт змінено // Правильно: розкидати (spread) обидва рівні const safeCopy = { ...user, settings: { ...user.settings, theme: "light" } }; console.log(user.settings.theme); // "dark" - захищено

Поверхнева копія (shallow copy) дублює тільки верхній рівень; вкладені об'єкти все одно вказують на ті самі адреси. Це класична пастка, яка підловлює навіть досвідчених розробників.

Помилка 3: Частина методів масивів мутує оригінал

javascript
// Ці методи змінюють оригінальний масив nums.push(4); nums.splice(0, 1); nums.sort(); // Ці повертають нові масиви const added = [...nums, 4]; const removed = nums.slice(1); const sorted = [...nums].sort();

Помилка 4: Мутація стану в React

javascript
const [items, setItems] = useState([1, 2, 3]); // Неправильно - React бачить те саме посилання, ре-рендер не відбудеться items.push(4); setItems(items); // Правильно - нове посилання, React бачить зміну setItems([...items, 4]);

Де зустрічається

  • React: props і state є імутабельними за конвенцією; useState очікує нові посилання для виявлення змін
  • Redux: редюсери повертають новий об'єкт стану на кожну дію
  • Immer.js: дозволяє писати код у стилі мутацій, але під капотом виробляє імутабельні оновлення
  • JavaScript додає нові імутабельні методи масивів: toSorted(), toReversed(), toSpliced(), with() - вони повертають нові масиви замість зміни оригіналу

Питання на співбесіді

Q: Чим const відрізняється від імутабельності?
A: const забороняє перепризначення змінної, але не забороняє змінювати сам об'єкт. const obj = {}; obj.prop = "value" - валідний код. Імутабельність стосується вмісту об'єкта, а не прив'язки змінної.

Q: Яка ціна продуктивності при створенні нових об'єктів скрізь?
A: Сучасні рушії JavaScript добре з цим справляються. Для великих або глибоко вкладених структур бібліотеки типу Immer використовують structural sharing - копіюють тільки змінений шлях, решта посилань залишається спільною. Витрати пам'яті залишаються під контролем.

Q: Як оновлювати глибоко вкладені об'єкти без нескінченних spread-операторів?
A: Immer.js. Пишеш produce(state, draft => { draft.user.address.city = "LA"; }) і отримуєш новий імутабельний стан без ручного розкидання на кожному рівні.

Q: (Для senior) Що таке structural sharing і як це використовує Immer?
A: Structural sharing означає, що незмінені гілки дерева даних повторно використовуються між версіями. При оновленні одного поля копіюються тільки об'єкти вздовж шляху до нього; всі інші вузли - ті самі посилання, що й раніше. Саме так Immer і Immutable.js тримають оновлення ефективними.

Приклади

Проміжний: Імутабельне оновлення стану в React

javascript
function UserProfile() { const [user, setUser] = useState({ name: "Alice", address: { city: "NYC", zip: "10001" } }); const updateCity = (newCity) => { setUser({ ...user, address: { ...user.address, city: newCity // змінюємо тільки це поле } }); }; return <button onClick={() => updateCity("LA")}>Move to LA</button>; }

Оновлення вкладеного поля вимагає spread і зовнішнього, і вкладеного об'єкта. Пропусти будь-який з них - і отримаєш мутацію, яку React не помітить.

Просунутий: Пастка з посиланнями на масиви

javascript
const items = [1, 2, 3]; const newItems = items; // посилання, не копія newItems.push(4); console.log(items); // [1, 2, 3, 4] - обидва змінились console.log(items === newItems); // true - той самий об'єкт у пам'яті // Поверхнева копія краще, але не ідеальна для вкладених структур const copy = items.slice(); copy.push(4); console.log(items); // [1, 2, 3] - верхній рівень у безпеці // Вкладені масиви все одно мають спільні посилання const matrix = [[1, 2], [3, 4]]; const shallowCopy = matrix.slice(); shallowCopy[0][0] = 99; console.log(matrix); // [[99, 2], [3, 4]] - внутрішній масив змінився // structuredClone вирішує проблему const deepCopy = structuredClone(matrix); deepCopy[0][0] = 99; console.log(matrix); // [[1, 2], [3, 4]] - оригінал у безпеці

structuredClone() доступний у сучасних браузерах і Node 17+. Для старіших середовищ Immer або Lodash cloneDeep вирішують ту саму задачу.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?