Чистые функции
Чистая функция — функция, которая: 1) при одинаковых аргументах всегда возвращает одинаковый результат (детерминированность); 2) не имеет побочных эффектов (не изменяет внешнее состояние).
Зачем нужно
Чистые функции предсказуемы, легко тестируются, безопасно кэшируются (мемоизация) и могут выполняться параллельно. Они — основа функционального программирования и ключ к написанию надёжного кода.
Где используется
Утилиты и хелперы, Redux reducers, React-компоненты, вычисления, форматирование, валидация, преобразование данных, тесты.
Предпосылки
Function Declaration, Парадигмы
Чистая vs нечистая
Чистая функция
function add(a, b) {
return a + b; // Всегда одинаковый результат, нет побочных эффектов
}
function formatPrice(cents) {
return `${(cents / 100).toFixed(2)} руб.`;
}
function getFullName(user) {
return `${user.firstName} ${user.lastName}`;
}
// Детерминированность: add(2, 3) ВСЕГДА вернёт 5
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 5
Нечистая функция
let total = 0;
function addToTotal(amount) {
total += amount; // Побочный эффект: мутация внешней переменной
return total;
}
console.log(addToTotal(5)); // 5
console.log(addToTotal(5)); // 10 — разный результат при одинаковом входе!
function getRandomItem(array) {
return array[Math.floor(Math.random * array.length)]; // Недетерминированная
}
function logAndReturn(value) {
console.log(value); // Побочный эффект: вывод в консоль
return value;
}
Признаки чистой функции
// 1. Зависит ТОЛЬКО от аргументов
function pure(x, y) {
return x * y + 1;
}
// 2. НЕ изменяет аргументы
function pureFilter(arr, predicate) {
return arr.filter(predicate); // filter возвращает НОВЫЙ массив
}
// 3. НЕ обращается к внешнему изменяемому состоянию
const TAX_RATE = 0.2; // OK — константа не меняется
function calcTax(price) {
return price * TAX_RATE;
}
// 4. НЕ производит побочных эффектов
// Нет: console.log, DOM, fetch, запись в файл, изменение глобальных переменных
Примеры чистых функций
// Форматирование
const capitalize = (str) =>
str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
// Вычисления
const clamp = (value, min, max) =>
Math.min(Math.max(value, min), max);
// Преобразование данных
const extractNames = (users) =>
users.map(u => u.name);
// Валидация
const isValidEmail = (email) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
// Создание нового объекта (не мутация)
const updateUser = (user, updates) => ({
...user,
...updates,
updatedAt: updates.updatedAt || user.updatedAt
});
Преимущества чистых функций
Тестирование
// Чистую функцию тестировать тривиально
function calcDiscount(price, percentage) {
return price - price * (percentage / 100);
}
// Тесты — просто вход → выход
console.assert(calcDiscount(100, 10) === 90);
console.assert(calcDiscount(200, 25) === 150);
console.assert(calcDiscount(0, 50) === 0);
// Не нужны моки, стабы, подготовка окружения
Мемоизация
// Чистые функции безопасно кэшировать
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn(...args));
return cache.get(key);
};
}
const expensiveCalc = memoize((n) => {
let result = 0;
for (let i = 0; i < n * 1e6; i++) result += Math.sqrt(i);
return result;
});
Композиция
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
const processUser = pipe(
(user) => ({ ...user, name: user.name.trim() }),
(user) => ({ ...user, email: user.email.toLowerCase() }),
(user) => ({ ...user, isValid: isValidEmail(user.email) })
);
const result = processUser({ name: ' Иван ', email: 'IVAN@MAIL.COM' });
// { name: 'Иван', email: 'ivan@mail.com', isValid: true }
Избегание мутации аргументов
// Плохо — мутирует входной массив
function sortBad(arr) {
return arr.sort((a, b) => a - b); // sort мутирует!
}
// Хорошо — создаёт новый массив
function sortGood(arr) {
return [...arr].sort((a, b) => a - b);
}
// Плохо — мутирует объект
function updateAgeBad(user, newAge) {
user.age = newAge; // мутация!
return user;
}
// Хорошо — новый объект
function updateAgeGood(user, newAge) {
return { ...user, age: newAge };
}
Redux reducer — пример чистой функции
function todosReducer(state = , action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, {
id: action.id,
text: action.text(),
completed: false
}];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
);
case 'REMOVE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
// Всегда возвращает НОВОЕ состояние, не мутирует старое
Частые ошибки
1. Скрытая мутация через методы массива
// sort, reverse, splice — мутируют!
const sortedBad = (arr) => arr.sort(); // мутация!
const sortedGood = (arr) => [...arr].sort(); // копия
// push, unshift — мутируют!
const addBad = (arr, item) => { arr.push(item); return arr; };
const addGood = (arr, item) => [...arr, item];
2. Date и Math.random — не чистые
// Зависимость от текущего времени
const greet = () => {
const hour = new Date.getHours();
return hour < 12 ? 'Доброе утро' : 'Добрый день';
};
// Чистая альтернатива — передать время как аргумент
const greetPure = (hour) =>
hour < 12 ? 'Доброе утро' : 'Добрый день';
3. Ссылка на внешнее изменяемое состояние
let config = { locale: 'ru' };
// Нечистая — зависит от внешнего config
const formatDate = (date) => date.toLocaleDateString(config.locale);
// Чистая — locale передан явно
const formatDatePure = (date, locale) => date.toLocaleDateString(locale);
Практика
- Определи, какие из 5 функций чистые, а какие нет — обоснуй
- Перепиши нечистую функцию с мутацией массива в чистую
- Напиши чистый reducer для управления списком задач
- Создай набор чистых утилит для работы со строками
- Мемоизируй чистую функцию и убедись, что кэш работает корректно