Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Незмінність та змінність у JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Мутабельність (mutability)** - це зміна об'єкта напряму. **Іммутабельність (immutability)** - створення нового об'єкта для кожної зміни, оригінал не зачіпається. ```js const user = { name: 'Alice' }; // Мутабельно: змінює на місці, всі посилання бачать зміну user.name = 'Bob'; // Іммутабельно: новий об'єкт, оригінал незмінний const updated = { ...user, name: 'Charlie' }; console.log(user.name); // 'Bob' console.log(updated.name); // 'Charlie' ``` **Ключове:** React порівнює стан за посиланням (`===`). Мутація того самого об'єкта не викличе ре-рендер. Для спільного стану завжди повертай нове посилання.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Мутабельність (mutability)** означає, що об'єкт можна змінювати безпосередньо після створення. **Іммутабельність (immutability)** означає, що для будь-якої зміни створюється новий об'єкт, а оригінал залишається недоторканим. ## Теорія ### TL;DR - Мутабельні об'єкти схожі на спільну дошку: хто має посилання, той змінює ті самі дані напряму - Іммутабельні оновлення схожі на ксерокопії: редагування дає нову копію, оригінал залишається як є - Головна різниця: мутація змінює спільну область пам'яті; іммутабельність виділяє нову пам'ять і розриває зв'язок між посиланнями - Використовуй іммутабельність для спільного стану (React props, Redux store); мутабельність - для приватних, перформанс-критичних операцій - `const` НЕ робить об'єкт іммутабельним. Він тільки блокує перепризначення змінної. ### Швидкий приклад ```js // Мутабельно: всі посилання бачать зміну const mutableUser = { name: 'Alice' }; const ref = mutableUser; mutableUser.name = 'Bob'; console.log(ref.name); // 'Bob' - спільне посилання змінилось // Іммутабельно: оригінал залишається незмінним const immutableUser = { name: 'Alice' }; const updated = { ...immutableUser, name: 'Bob' }; console.log(immutableUser.name); // 'Alice' - незмінний console.log(updated.name); // 'Bob' ``` Spread (`...`) копіює власні властивості в новий об'єкт за новою адресою пам'яті. Дві змінні тепер вказують на різні місця, тому зміни в одній не впливають на іншу. ### Ключова різниця Об'єкти в JavaScript - це посилальні типи. Мутабельна операція `obj.name = 'Bob'` перезаписує властивість у heap-слоті, який спільно використовують усі змінні, що вказують на `obj`. Іммутабельна операція `{ ...obj, name: 'Bob' }` виділяє новий простір у heap і shallow-копіює власні властивості туди. Вартість O(n), але оригінальне посилання повністю ізольоване. ### Коли що використовувати - Спільний UI-стан (React props, useState) - іммутабельність. React порівнює за посиланням (`===`), а не за вмістом. Мутація того самого об'єкта не викличе ре-рендер. - Redux store - іммутабельність. Дозволяє time-travel debugging і передбачувану історію стану. - Перформанс-критичні приватні дані (tight loops, великі буфери) - мутабельність. Немає overhead від копіювання. - Чисті функції і тестовані утиліти - іммутабельність. Передбачувані вхідні/вихідні дані, мемоїзація працює коректно. - Локальні змінні з одним власником - мутабельність. Простіший код, без витрат. ### Таблиця порівняння | Властивість | Мутабельність | Іммутабельність | |---|---|---| | Зміна даних | На місці в оригіналі | Створюється новий об'єкт | | Ре-рендер у React | Може не спрацювати (те саме посилання) | Завжди спрацьовує (нове посилання) | | Побічні ефекти | Можливі між посиланнями | Ізольовані за визначенням | | Налагодження | Важче простежити | Чітка історія змін | | Перформанс | Немає overhead від копіювання | O(n) вартість копіювання | | Типове використання | Приватні локальні дані, буфери | Спільний стан, Redux, props | ### Як це працює у V8 V8 зберігає об'єкти як структури у heap. `obj.prop = val` перезаписує слот властивості через разименування вказівника - без алокації, константний час. `{ ...obj }` запускає `OrdinaryObjectCreate` + `CopyDataProperties`: новий простір у heap, shallow-копія власних перелічуваних властивостей. Копія коштує O(n) відносно кількості властивостей. Але shallow означає, що вкладені об'єкти досі спільні - звідси найпоширеніша пастка зі spread. ### Типові помилки **Припускати, що spread робить глибоку копію:** ```js const state = { user: { profile: { friends: ['Alice'] } } }; const copy = { ...state }; // тільки shallow copy! copy.user.profile.friends.push('Bob'); console.log(state.user.profile.friends); // ['Alice', 'Bob'] - оригінал змінився ``` Spread копіює тільки верхній рівень властивостей. Вкладені об'єкти залишаються спільними посиланнями. Саме ця пастка з вкладеними посиланнями найчастіше застає розробників зненацька, коли вони вперше приходять у React-проект. Виправлення: `structuredClone(state)` (Node 17+, Chrome 98+) для повної глибокої копії. **Мутувати стан напряму у React:** ```js // Неправильно: те саме посилання, React пропускає ре-рендер const markDoneWrong = () => { todos[0].done = true; setTodos(todos); // shallow перевірка посилання провалюється - UI завис }; // Правильно: новий масив з новим об'єктом const markDoneCorrect = () => { setTodos(todos.map(todo => todo.id === 1 ? { ...todo, done: true } : todo )); }; ``` Reconciler React використовує shallow-порівняння посилань. Якщо посилання не змінилось, React пропускає ре-рендер повністю. Без помилки, без попередження - просто застарілий UI. **Розраховувати на `const` для іммутабельності:** ```js const arr = [1, 2]; arr.push(3); // працює без помилок console.log(arr); // [1, 2, 3] ``` `const` блокує перепризначення змінної, а не мутацію значення. Для shallow-іммутабельності використовуй `Object.freeze(arr)`, для вкладених структур - іммутабельні патерни. **Мутувати аргументи функції:** ```js // Неправильно: нечиста функція, ламає React.memo і Redux selectors function addItem(list, item) { list.push(item); return list; } // Правильно: повертає новий масив function addItem(list, item) { return [...list, item]; } ``` Мемоїзація покладається на стабільні посилання вхідних даних. Мутуєш вхідний масив - кеш мемо стає некоректним непередбачуваним чином. **Очікувати глибоку копію від `Object.assign`:** ```js const copy = Object.assign({}, state); // shallow copy.nested.arr.push(1); // мутує state.nested.arr ``` `Object.assign` копіює тільки власні перелічувані властивості верхнього рівня. Та сама пастка, що і зі spread. Використовуй `structuredClone` або `_.cloneDeep` з lodash, коли потрібна повна глибина. ### Де це зустрічається - React useState: `setState({ ...prev, name: 'Bob' })` - патерн з офіційних docs, створює нове посилання для reconciler - Redux Toolkit: `createSlice` використовує Immer всередині - проксує мутації на draft і виробляє іммутабельний патч - Immer: дозволяє писати `state.user.name = 'Bob'` всередині `produce()`, Immer автоматично перетворює це на іммутабельне оновлення - Lodash: `_.cloneDeep(obj)` у Express middleware для безпечного клонування тіл запитів - Node.js streams: внутрішні Buffer мутабельні для перформансу, але event payloads між хендлерами копіюються іммутабельно ### Питання на співбесіді **Q:** Що виведе цей код і чому: `const a = [1]; const b = a; a.push(2); console.log(b);` **A:** `[1, 2]`. Масиви - посилальні типи. `b` і `a` вказують на те саме місце у heap. `push` мутує на місці, тому `b` відображає зміну. **Q:** Чому React не робить ре-рендер при прямій мутації стану? **A:** Reconciler React порівнює посилання через `===`. Якщо `setState` отримує те саме посилання, перевірка проходить як «без змін» і цикл рендерингу пропускається. **Q:** Яка вартість іммутабельності для перформансу? **A:** Shallow-копія через spread коштує O(n), де n - кількість власних властивостей. Бібліотеки на кшталт Immer використовують structural sharing: копіюється тільки змінений шлях, незмінені гілки зберігають оригінальні посилання. Для великого вкладеного стану це суттєво зменшує витрати на алокацію. **Q:** Чим `Object.freeze` відрізняється від справжньої іммутабельності? **A:** `Object.freeze` робить один рівень об'єкта read-only без виділення нового об'єкта. Кидає помилку у strict mode, тихо ігнорує запис в sloppy mode. Вкладені об'єкти залишаються мутабельними. Справжня іммутабельність у Redux-патернах означає нове посилання при кожному оновленні. **Q:** Коли обирати `structuredClone` замість `JSON.parse(JSON.stringify(x))`? **A:** Майже завжди. `structuredClone` підтримує циклічні посилання, `Date`, `Map`, `Set` і типізовані масиви. `JSON.parse/stringify` втрачає `undefined` і функції, перетворює `Date` на рядки і не вміє в циклічні посилання. Єдина причина для `JSON.parse/stringify` - підтримка середовищ, старших за Chrome 98 або Node 17, без поліфілу. **Q (senior):** Як draft proxy в Immer забезпечує ефективні іммутабельні оновлення? **A:** Immer обгортає стан у Proxy. Всередині `produce()` він відстежує, які шляхи були доступні і змінені. При комміті застосовує structural sharing: незмінені піддерева зберігають оригінальні посилання, копіюються тільки змінені вузли. Оновлення одного поля у великому об'єкті коштує O(depth), а не O(total properties). Тому Redux Toolkit використовує Immer за замовчуванням замість ручного spread на кожному рівні вкладеності. ## Приклади ### Пастка спільного посилання ```js const original = { name: 'Alice', scores: [10, 20] }; const copy = { ...original }; copy.name = 'Bob'; copy.scores.push(30); // мутує original.scores! console.log(original.name); // 'Alice' - верхній рівень в порядку console.log(original.scores); // [10, 20, 30] - вкладений масив змінився ``` Spread створює новий об'єкт верхнього рівня, але вкладені властивості досі вказують на ті самі посилання. Для безпечного оновлення вкладеного масиву копіюй і його: `{ ...original, scores: [...original.scores, 30] }`. ### Оновлення todos у React ```js const [todos, setTodos] = useState([ { id: 1, text: 'Learn JS', done: false } ]); // Неправильно: мутує оригінал, React пропускає ре-рендер const markDoneWrong = () => { todos[0].done = true; setTodos(todos); // те саме посилання - UI не оновиться }; // Правильно: новий масив з новим об'єктом для зміненого елемента const markDoneCorrect = () => { setTodos(todos.map(todo => todo.id === 1 ? { ...todo, done: true } : todo )); }; ``` `map` повертає новий масив. Spread всередині створює новий об'єкт todo. React отримує нове посилання, бачить зміну і запускає ре-рендер. ### Глибоке клонування через structuredClone ```js const state = { user: { profile: { friends: ['Alice'] } } }; // Node 17+, Chrome 98+ const safeCopy = structuredClone(state); safeCopy.user.profile.friends.push('Bob'); console.log(state.user.profile.friends); // ['Alice'] - оригінал безпечний console.log(safeCopy.user.profile.friends); // ['Alice', 'Bob'] ``` `structuredClone` робить повну глибоку копію. Підтримує вкладені об'єкти, масиви, `Date`, `Map`, `Set` і циклічні посилання. Функції та символи не копіює - пам'ятай про це для об'єктів з методами.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.