Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Чисті функції та побічні ефекти в JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Чиста функція** - функція, яка для однакових аргументів завжди повертає однаковий результат і нічого не змінює поза собою. ```javascript const add = (a, b) => a + b; // Чиста: завжди 5 для (2, 3) let total = 0; const addToTotal = (n) => { total += n; return total; }; // Нечиста: мутує total ``` **Ключове:** чисті функції можна мемоізувати, тестувати без моків і викликати в будь-якому порядку.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Чиста функція** - функція, яка завжди повертає однаковий результат для однакових аргументів і нічого не змінює поза собою. ## Теорія ### TL;DR - Аналогія: чиста функція як торговий автомат. Ті самі монети - та сама кола. Він не чіпає запаси магазину. - Головна різниця: чиста - результат залежить тільки від аргументів; нечиста - від часу, глобальних змінних, зовнішнього стану. - Правило вибору: чиста для математики, логіки, трансформацій даних. Нечиста тільки там де не уникнути (API, DOM, таймери). - `const` не означає чистоту. Об'єкт оголошений через `const` можна мутувати. - Мемоізація (`React.memo`, `useMemo`) працює коректно тільки з чистими функціями. ### Швидкий приклад ```javascript // Чиста: однаковий вхід завжди дає однаковий вихід const add = (a, b) => a + b; add(2, 3); // 5, завжди // Нечиста: результат залежить від зовнішньої змінної discount let discount = 0.1; const getPrice = (price) => price * (1 - discount); getPrice(100); // 90 сьогодні, 80 якщо discount стане 0.2 // Чистий варіант: передаємо discount як аргумент const getPricePure = (price, discount) => price * (1 - discount); getPricePure(100, 0.1); // 90, завжди для цих аргументів ``` Нечиста `getPrice` читає `discount` із зовнішньої області видимості (scope). Зміниш цю змінну в іншому тесті або в іншому місці коду - і поведінка функції зміниться. Саме в цьому проблема. ### Головна різниця Чиста функція як математична формула: результат повністю визначається аргументами і більше нічим. Побічні ефекти (side effects) ламають цю гарантію - через читання зовнішнього стану (`Date.now()`, глобальна змінна) або запис до нього (`array.push()`, `localStorage.setItem()`). Після цього не можна гарантувати що однаковий виклик дасть однаковий результат. ### Коли використовувати - Математичні та логічні обчислення: чиста. Без винятків. - Сортування, фільтрація, трансформація масивів: чиста (повертай новий масив, не мутуй вхідні дані). - Рендеринг React-компонентів, Redux-редюсери: чиста. Алгоритм узгодження React на це покладається. - API-запити, таймери, оновлення DOM, логування: нечиста за природою. Ізолюй їх на межі системи. - Потрібна мемоізація: тільки чиста. Кешування мовчки ламається на нечистих функціях. ### Як V8 це обробляє V8 (рушій Chrome і Node.js) застосовує inline caching і escape analysis до функцій, які не читають зовнішній стан і не мутують нічого. Він може розміщувати повернені значення на стеку замість купи і пропускати зайві пошуки властивостей. Як тільки функція торкається зовнішнього стану або виконує I/O - V8 скасовує ці оптимізації. Тому Redux вимагає чистих редюсерів, а Immer використовує Proxy-пастки щоб симулювати мутації і при цьому повертати новий об'єкт стану. ### Типові помилки **Помилка 1: `Array.map` завжди чистий** `map` повертає новий масив, але колбек може виконувати будь-який код. Якщо він чіпає зовнішній стан - весь виклик нечистий. ```javascript let calls = 0; [1, 2].map(n => { calls++; return n * 2; }); // Новий масив повернуто, але calls === 2 тепер. Побічний ефект. ``` Виправлення: тримай колбеки без стану. Всі залежності передавай як аргументи. **Помилка 2: `const` = незмінність** `const` блокує перепризначення. Але не блокує мутацію самого об'єкта чи масиву. ```javascript const arr = [1, 2]; arr.push(3); // arr тепер [1, 2, 3]. Мутація є. ``` Виправлення: spread для нового масиву або `Object.freeze` для поверхневої незмінності. **Помилка 3: замикання (closure) з мутабельним станом** Замикання захоплює змінні із зовнішньої області видимості. Якщо вони мутабельні - функція нечиста, навіть якщо виглядає чисто. ```javascript let x = 1; const increment = () => x++; // Читає і змінює зовнішній x. Нечиста. ``` Виправлення: передавай значення як параметр замість захоплення. **Помилка 4: async-функція може бути чистою** `fetch` завжди нечистий: залежить від мережі, часу і стану сервера. Обгортка не змінює цього. ```javascript const getData = () => fetch('/api'); // Нечиста. Мережа - зовнішній стан. ``` Виправлення: мокай I/O в тестах. Тримай нечистий код на межі системи. **Помилка 5: `Object.freeze` заморожує глибоко** `Object.freeze` заморожує тільки перший рівень. Вкладені об'єкти залишаються мутабельними. ```javascript const state = Object.freeze({ user: { name: 'Alex' } }); state.user.name = 'Bob'; // Спрацює. Вкладений об'єкт не заморожений. ``` Виправлення: розгортай кожен рівень при оновленні: `{ ...state, user: { ...state.user, name: 'Bob' } }`. ### Де зустрічається - React: чисті компоненти коректно оптимізуються через `React.memo`. Нечисті можуть давати застарілий рендер. - Redux: редюсери завжди чисті. Immer дозволяє писати мутативний синтаксис і при цьому повертати новий стан. - Node/Express: чисті валідатори (схеми Joi) запускаються перед записом у БД. Валідація чиста, запис - ні. - Ramda/Lodash-fp: побудовані навколо чистих функцій. `R.map` і `R.filter` не мутують вхідні дані. Якось я дві години шукав баг у розрахунку суми в кошику. Виявилось, функція читала глобальний прапор знижки, який обнулявся в іншому тесті. Зробив функцію чистою - передав знижку аргументом. Баг і зламаний тест зникли за одну зміну. ### Питання на співбесіді **Q:** Чи може функція з `console.log` всередині бути чистою? **A:** Ні. `console.log` пише в зовнішній потік I/O. Це побічний ефект, незалежно від того що повертає функція. **Q:** Чи є `Array.sort()` чистим методом? **A:** Ні. Він мутує оригінальний масив. Використовуй `[...arr].sort()` щоб отримати відсортовану копію і залишити оригінал недоторканим. **Q:** Чому `React.memo` не працює з нечистими компонентами? **A:** `React.memo` пропускає ре-рендер коли пропси не змінились. Нечистий компонент може повернути інший результат для тих самих пропсів (наприклад, читаючи `Date.now()`), і кешований результат стане неправильним. **Q:** Redux Toolkit використовує Immer в `createSlice`. Чи роблять це редюсери нечистими? **A:** Ні. Immer перехоплює мутації через Proxy і повертає новий об'єкт стану. Для Redux редюсер залишається чистим: отримує стан, повертає новий. V8 не скасовує оптимізації бо справжня мутація не виходить за межі функції. **Q:** Що таке референційна прозорість (referential transparency)? **A:** Чиста функція є референційно прозорою: будь-який її виклик можна замінити результатом і програма поведеться так само. Саме це робить мемоізацію та паралельне виконання безпечними. ## Приклади ### Базовий: робимо функцію ціни чистою ```javascript // Нечиста: читає зовнішню змінну discount let discount = 0.1; const getPriceImpure = (price) => price * (1 - discount); console.log(getPriceImpure(100)); // 90 discount = 0.2; console.log(getPriceImpure(100)); // 80 - той самий виклик, інший результат // Чиста: всі входи явні const getPricePure = (price, discount) => price * (1 - discount); console.log(getPricePure(100, 0.1)); // 90, завжди console.log(getPricePure(100, 0.2)); // 80, завжди - передбачувано ``` Кожна зовнішня залежність стає параметром. Тепер функцію можна тестувати з будь-якими значеннями без налаштування або скидання стану. ### Середній: фільтрація списку задач у React ```javascript // Нечиста: мутує оригінальний масив (ламає узгодження React) const filterCompletedImpure = (todos) => { todos.forEach((todo, i) => { if (todo.complete) todos.splice(i, 1); }); return todos; }; // Чиста: повертає новий масив, оригінал незмінний const filterCompleted = (todos) => todos.filter(todo => !todo.complete); const todos = [ { id: 1, complete: true }, { id: 2, complete: false }, ]; console.log(filterCompleted(todos)); // [{ id: 2, complete: false }] console.log(todos); // [{ id: 1, complete: true }, { id: 2, complete: false }] - незмінний ``` React порівнює попередні та нові пропси щоб вирішити чи перерендерити. Якщо мутувати масив замість повернення нового - посилання залишається тим самим і React може пропустити оновлення. ### Складний: поверхневе заморожування vs глибоке клонування в Redux-стилі ```javascript const state = { count: 5, user: { name: 'Alex' } }; // Object.freeze - поверхневий const frozen = Object.freeze({ ...state }); frozen.count = 10; // Тихий fail у звичайному режимі, TypeError у strict frozen.user.name = 'Bob'; // Спрацьовує. Вкладений об'єкт НЕ заморожений. // Чисте оновлення через spread const nextState = { ...state, count: state.count + 1 }; console.log(nextState); // { count: 6, user: { name: 'Alex' } } console.log(state); // { count: 5, user: { name: 'Alex' } } - незмінний // Глибоке оновлення: розгортаємо кожен рівень const nextStateDeep = { ...state, user: { ...state.user, name: 'Bob' }, }; console.log(nextStateDeep.user); // { name: 'Bob' } console.log(state.user); // { name: 'Alex' } - безпечно ``` У продакшені Immer обробляє вкладені оновлення автоматично. Розуміння чому він існує показує що ти знаєш обмеження, а не просто API.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.