Пошук уроків, статей та іншого контенту
Розберіть різницю між чистими й нечистими функціями та поняття побічних ефектів. Дізнайтеся, як зміна зовнішнього стану, робота з DOM, мережею або сховищем впливає на передбачуваність і тестованість коду.
Чиста функція — це функція, яка відповідає двом умовам:
Для однакових аргументів вона завжди повертає однаковий результат.
Вона не має побічних ефектів — не змінює стан поза власною областю видимості.
Наприклад:
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 5Функція add є чистою:
результат залежить лише від a і b;
вона не читає глобальні змінні;
не змінює аргументи;
не записує дані у файл, сховище чи DOM;
не виконує мережевих запитів;
не виводить нічого в консоль.
Таку функцію легко повторно викликати, тестувати й переносити в інші частини програми.
Вираз має референційну прозорість, якщо його можна замінити результатом обчислення без зміни поведінки програми.
const total = add(2, 3);Оскільки add(2, 3) завжди повертає 5, цей код еквівалентний такому:
const total = 5;Саме тому чисті функції добре підходять для композиції:
function multiply(a, b) {
return a * b;
}
const result = multiply(add(2, 3), 4);
console.log(result); // 20Нечиста функція порушує хоча б одну з умов чистої функції:
залежить від зовнішнього стану;
змінює зовнішній стан;
повертає різний результат для однакових аргументів;
виконує операції введення-виведення;
взаємодіє із середовищем виконання.
Наприклад, функція використовує глобальну змінну:
let taxRate = 0.2;
function calculateTax(price) {
return price * taxRate;
}Результат залежить не лише від price, а й від зовнішньої змінної taxRate:
console.log(calculateTax(100)); // 20
taxRate = 0.3;
console.log(calculateTax(100)); // 30Аргументи функції не змінилися, але результат став іншим.
Кращий варіант — передавати всі залежності через параметри:
function calculateTax(price, taxRate) {
return price * taxRate;
}
console.log(calculateTax(100, 0.2)); // 20
console.log(calculateTax(100, 0.3)); // 30Тепер функція є чистою.
Побічний ефект — це будь-яка помітна зміна або взаємодія функції з навколишнім середовищем, яка не є її поверненим значенням.
До побічних ефектів належать:
зміна глобальних змінних;
зміна об'єктів або масивів, отриманих через аргументи;
зміна DOM;
виконання мережевих запитів;
читання або запис у localStorage;
робота з файлами;
виведення в консоль;
запуск таймерів;
генерація випадкових значень;
отримання поточної дати або часу;
робота з API браузера.
Побічні ефекти не є забороненими. Реальні програми без них не обходяться. Важливо локалізувати їх і не змішувати безпосередню логіку обчислень із взаємодією зі світом.
Об'єкти та масиви передаються у функції за значенням, але значенням є посилання на об'єкт. Тому функція може змінити початковий об'єкт.
function addRole(user) {
user.role = "admin";
return user;
}
const user = {
name: "Олена"
};
addRole(user);
console.log(user);
// { name: "Олена", role: "admin" }Функція змінила об'єкт, створений за її межами. Це побічний ефект.
Чистіший варіант — повернути новий об'єкт:
function addRole(user) {
return {
...user,
role: "admin"
};
}
const user = {
name: "Олена"
};
const updatedUser = addRole(user);
console.log(user);
// { name: "Олена" }
console.log(updatedUser);
// { name: "Олена", role: "admin" }Для масивів замість мутаційних методів часто використовують методи, які повертають новий масив:
function addTask(tasks, task) {
return [...tasks, task];
}
const tasks = ["Вивчити змінні"];
const updatedTasks = addTask(tasks, "Вивчити функції");
console.log(tasks);
// ["Вивчити змінні"]
console.log(updatedTasks);
// ["Вивчити змінні", "Вивчити функції"]Деякі методи масивів змінюють початковий масив:
push;
pop;
shift;
unshift;
splice;
sort;
reverse.
Інші повертають новий результат:
map;
filter;
slice;
concat;
toSorted;
toReversed.
Наприклад:
function sortNumbers(numbers) {
return [...numbers].sort((a, b) => a - b);
}
const numbers = [3, 1, 2];
const sortedNumbers = sortNumbers(numbers);
console.log(numbers); // [3, 1, 2]
console.log(sortedNumbers); // [1, 2, 3]Копіювання перед сортуванням захищає початковий масив.
Функції, які використовують поточний час або випадкові значення, зазвичай є нечистими:
function getCurrentYear() {
return new Date().getFullYear();
}
function getRandomNumber() {
return Math.random();
}Виклик таких функцій у різний момент може дати різний результат навіть без зміни аргументів.
Це не означає, що їх не можна використовувати. Залежність можна передати через параметр:
function getYearFromDate(date) {
return date.getFullYear();
}
const date = new Date("2025-06-15");
console.log(getYearFromDate(date)); // 2025Таку функцію набагато легше тестувати, оскільки тест сам визначає дату.
Зміна DOM є побічним ефектом:
function showMessage(message) {
document.querySelector("#message").textContent = message;
}Функція не просто обчислює значення — вона змінює стан сторінки.
Зручний підхід — відокремити підготовку даних від оновлення DOM:
function formatWelcomeMessage(name) {
return `Вітаємо, ${name}!`;
}
function renderWelcomeMessage(element, name) {
element.textContent = formatWelcomeMessage(name);
}
const messageElement = document.querySelector("#message");
renderWelcomeMessage(messageElement, "Олена");formatWelcomeMessage є чистою функцією. renderWelcomeMessage має побічний ефект, але він обмежений конкретною відповідальністю — оновленням елемента.
Мережевий запит змінює стан взаємодії програми із зовнішньою системою:
async function loadUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}Результат залежить від:
доступності сервера;
мережевого з'єднання;
поточного стану даних на сервері;
помилок авторизації;
часу відповіді.
Тому loadUser є нечистою функцією.
Запис у localStorage також є побічним ефектом:
function saveTheme(theme) {
localStorage.setItem("theme", theme);
}Часто зручно розділяти перетворення даних і сам запис:
function serializeSettings(settings) {
return JSON.stringify(settings);
}
function saveSettings(settings) {
const serializedSettings = serializeSettings(settings);
localStorage.setItem("settings", serializedSettings);
}serializeSettings можна тестувати окремо без браузера, а saveSettings залишити невеликою функцією, яка відповідає лише за взаємодію зі сховищем.
Розглянемо обробку списку замовлень:
function calculateOrderTotal(items, taxRate) {
const subtotal = items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
return subtotal + subtotal * taxRate;
}
function renderOrderTotal(element, total) {
element.textContent = `Разом: ${total.toFixed(2)} грн`;
}
const items = [
{ name: "Книга", price: 300, quantity: 2 },
{ name: "Зошит", price: 50, quantity: 3 }
];
const total = calculateOrderTotal(items, 0.2);
console.log(total); // 870
// Виклик у браузері:
// renderOrderTotal(document.querySelector("#total"), total);Функція calculateOrderTotal:
не змінює items;
не звертається до DOM;
не читає глобальні змінні;
повертає однаковий результат для однакових аргументів.
Функція renderOrderTotal має побічний ефект, оскільки змінює DOM. Проте обидві функції мають чіткі та невеликі обов'язки.
Якщо функція чиста, її поведінку можна визначити лише за аргументами. Не потрібно перевіряти стан глобальних змінних або порядок попередніх викликів.
Чисті функції тестуються без складного налаштування середовища:
function formatPrice(price, currency) {
return `${price.toFixed(2)} ${currency}`;
}
console.assert(
formatPrice(12.5, "грн") === "12.50 грн"
);Для тестування не потрібні браузер, сервер чи база даних.
Функцію без прихованих залежностей простіше використовувати в різних модулях, компонентах або навіть проєктах.
Якщо функція чиста, результат її виклику можна кешувати. Такий підхід називається мемоізацією.
function square(number) {
return number * number;
}
const cache = new Map();
function memoizedSquare(number) {
if (!cache.has(number)) {
cache.set(number, square(number));
}
return cache.get(number);
}
console.log(memoizedSquare(5)); // 25
console.log(memoizedSquare(5)); // 25Кешування нечистої функції може бути небезпечним, оскільки її результат може залежати від часу, мережі або зовнішнього стану.
Повністю усунути побічні ефекти неможливо: програма повинна читати введення, показувати дані користувачу й взаємодіяти із сервером.
Корисно дотримуватися таких принципів:
Передавайте залежності явно.
Замість читання глобальної змінної передавайте значення параметром.
Відділяйте обчислення від взаємодії із середовищем.
Спочатку підготуйте дані, а потім передайте їх DOM, API або сховищу.
Не змінюйте аргументи без необхідності.
Для об'єктів і масивів повертайте копії.
Робіть функції з побічними ефектами малими.
Невелику функцію для запису в сховище легше контролювати, ніж велику функцію, яка одночасно обробляє дані та змінює інтерфейс.
Називайте функції відповідно до їхньої поведінки.
Назви saveUser, renderList, sendRequest явно натякають на побічні ефекти.
Контролюйте порядок виконання.
Побічні ефекти часто важливі саме через порядок: спочатку потрібно отримати дані, потім відрендерити їх.
Функція може щось повертати й водночас мати побічний ефект:
let count = 0;
function getNextCount() {
count += 1;
return count;
}Вона повертає число, але змінює зовнішню змінну.
function activate(user) {
user.active = true;
return user;
}Повернення того самого об'єкта не робить функцію чистою. Початковий об'єкт уже було змінено.
sort без копіюванняfunction sortUsers(users) {
return users.sort();
}sort змінює початковий масив. Безпечніше:
function sortUsers(users) {
return [...users].sort();
}let discount = 0.1;
function getFinalPrice(price) {
return price * (1 - discount);
}Залежність краще зробити явною:
function getFinalPrice(price, discount) {
return price * (1 - discount);
}async сама по собі не робить функцію нечистою. Нечистою є функція через залежність від зовнішнього середовища або побічні ефекти.
Наприклад, асинхронна функція може виконувати чисте перетворення:
async function doubleAsync(number) {
return number * 2;
}Вона повертає Promise, але її результат усе ще залежить лише від аргументу. Натомість fetch додає залежність від мережі.
Чиста функція для однакових аргументів завжди повертає однаковий результат.
Чиста функція не змінює зовнішній стан.
Побічні ефекти охоплюють мутацію об'єктів, роботу з DOM, мережею, сховищем, часом і випадковими значеннями.
Об'єкти та масиви варто не змінювати напряму, а копіювати й повертати нові значення.
Залежності краще передавати через параметри, а не приховувати в глобальних змінних.
Побічні ефекти необхідні в реальних програмах, але їх варто ізолювати в невеликих функціях.
Розділення чистої логіки та побічних ефектів покращує передбачуваність, тестованість і підтримуваність коду.