Що таке імутабельність?
Імутабельність (immutability) - коли ти створив значення, ти його не змінюєш. Замість цього створюєш нове значення з потрібними змінами, а оригінал залишається як є.
Теорія
TL;DR
- Як фото: оригінал не редагуєш, але можеш зробити нову копію з іншими налаштуваннями
- Мутація змінює об'єкт за тією ж адресою в пам'яті; імутабельність виділяє нову адресу
- React і Redux порівнюють посилання на об'єкти, а не їх вміст, тому без імутабельності вони не побачать змін
- Використовуй, коли кілька частин коду посилаються на один об'єкт, або коли треба передбачувано відстежувати зміни стану
Короткий приклад
// Мутація - змінює оригінал
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: Присвоєння - це не копія
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: Поверхнева копія не захищає вкладені об'єкти
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: Частина методів масивів мутує оригінал
// Ці методи змінюють оригінальний масив
nums.push(4);
nums.splice(0, 1);
nums.sort();
// Ці повертають нові масиви
const added = [...nums, 4];
const removed = nums.slice(1);
const sorted = [...nums].sort();Помилка 4: Мутація стану в React
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
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 не помітить.
Просунутий: Пастка з посиланнями на масиви
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 вирішують ту саму задачу.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.