Чистая функция — это функция, которая:
// Чистая функция
function add(a, b) {
return a + b; // Всегда одинаковый результат для одинаковых a и b
}
// Нечистая функция
let counter = 0;
function increment() {
return ++counter; // Зависит от внешней переменной
}
Чистая функция (Pure Function) — это фундаментальная концепция функционального программирования, которая определяет функцию как “математическую” операцию. Такие функции являются основой предсказуемого и надежного кода.
// Математические операции
function multiply(a, b) {
return a * b;
}
// Работа со строками
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
// Работа с массивами (без мутации)
function filterEvenNumbers(numbers) {
return numbers.filter(num => num % 2 === 0);
}
// Работа с объектами (без мутации)
function updateUserAge(user, newAge) {
return { ...user, age: newAge };
}
// Сложные вычисления
function calculateTax(price, taxRate) {
return price * (taxRate / 100);
}
// Зависит от текущего времени
function getCurrentTimestamp() {
return Date.now(); // Каждый вызов даёт разный результат
}
// Зависит от внешней переменной
let discount = 0.1;
function applyDiscount(price) {
return price * (1 - discount); // Результат зависит от внешней переменной
}
// Использует случайные числа
function generateRandomId() {
return Math.random().toString(36); // Всегда разный результат
}
// Зависит от DOM
function getElementWidth(elementId) {
return document.getElementById(elementId).offsetWidth;
}
Побочные эффекты — это любые изменения состояния программы или взаимодействие с внешним миром:
// Возвращает новый массив, не изменяя исходный
function addItemToArray(array, item) {
return [...array, item];
}
// Возвращает новый объект, не изменяя исходный
function updateUserProfile(user, updates) {
return {
...user,
...updates,
updatedAt: new Date().toISOString()
};
}
// Вычисления без изменения внешнего состояния
function calculateMonthlyPayment(principal, rate, months) {
const monthlyRate = rate / 12 / 100;
const payment = principal *
(monthlyRate * Math.pow(1 + monthlyRate, months)) /
(Math.pow(1 + monthlyRate, months) - 1);
return Math.round(payment * 100) / 100;
}
// Изменяет глобальную переменную
let totalSales = 0;
function addSale(amount) {
totalSales += amount; // Побочный эффект!
return totalSales;
}
// Изменяет переданный объект
function updateUser(user, newData) {
user.name = newData.name; // Мутация!
user.updatedAt = Date.now();
return user;
}
// Выводит в консоль
function debugCalculation(a, b) {
const result = a + b;
console.log(`${a} + ${b} = ${result}`); // Побочный эффект!
return result;
}
// Изменяет DOM
function updateCounter(value) {
document.getElementById('counter').textContent = value; // Побочный эффект!
return value;
}
// Чистая функция - всегда предсказуемый результат
function formatPrice(price, currency = 'RUB') {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: currency
}).format(price);
}
// Можно быть уверенным в результате
console.log(formatPrice(1000)); // "1 000,00 ₽"
console.log(formatPrice(1000)); // "1 000,00 ₽" - всегда одинаково
// Чистую функцию легко тестировать
function calculateDiscount(price, discountPercent) {
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('Скидка должна быть от 0 до 100%');
}
return price * (discountPercent / 100);
}
// Простые тесты
function testCalculateDiscount() {
console.assert(calculateDiscount(1000, 10) === 100);
console.assert(calculateDiscount(500, 20) === 100);
console.assert(calculateDiscount(0, 50) === 0);
try {
calculateDiscount(1000, -5);
console.assert(false, 'Должна быть ошибка');
} catch (e) {
console.assert(e.message.includes('Скидка должна быть'));
}
}
// Чистые функции можно мемоизировать
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Дорогая чистая функция
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Мемоизированная версия
const memoizedFibonacci = memoize(fibonacci);
console.log(memoizedFibonacci(40)); // Вычисляется
console.log(memoizedFibonacci(40)); // Берётся из кэша
// Чистые функции безопасны для параллельного выполнения
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Чистая функция для обработки
function processNumber(num) {
return num * num + Math.sqrt(num);
}
// Можно безопасно использовать в Promise.all
const promises = numbers.map(num =>
Promise.resolve(processNumber(num))
);
Promise.all(promises).then(results => {
console.log(results); // Результат предсказуем
});
// Чистые функции валидации
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function isValidPassword(password) {
return password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password);
}
function validateUser(userData) {
const errors = [];
if (!userData.email || !isValidEmail(userData.email)) {
errors.push('Некорректный email');
}
if (!userData.password || !isValidPassword(userData.password)) {
errors.push('Пароль должен содержать минимум 8 символов, включая заглавные и строчные буквы, цифры');
}
return {
isValid: errors.length === 0,
errors
};
}
// Чистые функции для работы с данными
function normalizeUser(rawUser) {
return {
id: rawUser.id,
name: rawUser.full_name?.trim() || '',
email: rawUser.email?.toLowerCase() || '',
age: parseInt(rawUser.age) || 0,
isActive: rawUser.status === 'active'
};
}
function groupUsersByAge(users) {
return users.reduce((groups, user) => {
const ageGroup = Math.floor(user.age / 10) * 10;
const groupKey = `${ageGroup}-${ageGroup + 9}`;
if (!groups[groupKey]) {
groups[groupKey] = [];
}
groups[groupKey].push(user);
return groups;
}, {});
}
function calculateUserStats(users) {
return {
total: users.length,
active: users.filter(user => user.isActive).length,
averageAge: users.reduce((sum, user) => sum + user.age, 0) / users.length,
ageGroups: groupUsersByAge(users)
};
}
// Композиция чистых функций
function pipe(...functions) {
return function(value) {
return functions.reduce((acc, fn) => fn(acc), value);
};
}
function compose(...functions) {
return function(value) {
return functions.reduceRight((acc, fn) => fn(acc), value);
};
}
// Чистые функции для обработки строк
function trim(str) {
return str.trim();
}
function toLowerCase(str) {
return str.toLowerCase();
}
function removeSpaces(str) {
return str.replace(/\s+/g, '');
}
// Композиция функций
const normalizeString = pipe(
trim,
toLowerCase,
removeSpaces
);
console.log(normalizeString(' Hello World ')); // "helloworld"
// Добавление элемента
function addItem(array, item) {
return [...array, item];
}
// Удаление элемента по индексу
function removeItemByIndex(array, index) {
return array.filter((_, i) => i !== index);
}
// Обновление элемента
function updateItem(array, index, newItem) {
return array.map((item, i) => i === index ? newItem : item);
}
// Сортировка (не изменяет исходный массив)
function sortBy(array, keyFn) {
return [...array].sort((a, b) => {
const aKey = keyFn(a);
const bKey = keyFn(b);
return aKey < bKey ? -1 : aKey > bKey ? 1 : 0;
});
}
// Группировка
function groupBy(array, keyFn) {
return array.reduce((groups, item) => {
const key = keyFn(item);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {});
}
// Обновление свойства объекта
function updateProperty(obj, key, value) {
return { ...obj, [key]: value };
}
// Удаление свойства
function removeProperty(obj, key) {
const { [key]: removed, ...rest } = obj;
return rest;
}
// Глубокое обновление вложенного объекта
function updateNestedProperty(obj, path, value) {
const [head, ...tail] = path;
if (tail.length === 0) {
return { ...obj, [head]: value };
}
return {
...obj,
[head]: updateNestedProperty(obj[head] || {}, tail, value)
};
}
// Слияние объектов
function mergeObjects(...objects) {
return Object.assign({}, ...objects);
}
// Пример использования
const user = {
id: 1,
profile: {
name: 'Иван',
settings: {
theme: 'dark',
notifications: true
}
}
};
const updatedUser = updateNestedProperty(
user,
['profile', 'settings', 'theme'],
'light'
);
// Эти операции по определению нечистые
// Логирование
function logError(error) {
console.error('Ошибка:', error.message);
// Отправка в систему мониторинга
errorTracker.send(error);
}
// Работа с DOM
function updateUI(data) {
document.getElementById('content').innerHTML = data.html;
document.title = data.title;
}
// HTTP запросы
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// Работа с localStorage
function saveToStorage(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
// Разделяем чистую логику и побочные эффекты
// Чистая функция для подготовки данных
function prepareUserData(rawData) {
return {
id: rawData.id,
name: rawData.name.trim(),
email: rawData.email.toLowerCase(),
isValid: isValidEmail(rawData.email)
};
}
// Нечистая функция для сохранения
function saveUser(userData) {
// Побочный эффект изолирован
return fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
}
// Композиция: чистая подготовка + нечистое сохранение
async function createUser(rawData) {
const userData = prepareUserData(rawData);
if (!userData.isValid) {
throw new Error('Некорректные данные пользователя');
}
return await saveUser(userData);
}
Аспект | Чистые функции | Нечистые функции |
---|---|---|
Предсказуемость | Всегда одинаковый результат | Результат может меняться |
Тестирование | Легко тестировать | Требуют моков и заглушек |
Отладка | Простая отладка | Сложная отладка |
Кэширование | Можно мемоизировать | Нельзя кэшировать |
Параллелизм | Безопасны для параллельного выполнения | Могут вызывать race conditions |
Рефакторинг | Легко рефакторить | Сложно рефакторить |
Понимание кода | Легко понять логику | Нужно учитывать контекст |
Переиспользование | Высокая переиспользуемость | Ограниченная переиспользуемость |
// ❌ Плохо: смешивание чистой логики и побочных эффектов
function processAndSaveUser(userData) {
// Чистая логика
const normalizedData = {
name: userData.name.trim(),
email: userData.email.toLowerCase()
};
// Побочный эффект
console.log('Обработка пользователя:', normalizedData.name);
// Ещё побочный эффект
database.save(normalizedData);
return normalizedData;
}
// ✅ Хорошо: разделение ответственности
function normalizeUserData(userData) {
return {
name: userData.name.trim(),
email: userData.email.toLowerCase()
};
}
function logUserProcessing(userData) {
console.log('Обработка пользователя:', userData.name);
}
function saveUserToDatabase(userData) {
return database.save(userData);
}
// Композиция
function processUser(userData) {
const normalizedData = normalizeUserData(userData);
logUserProcessing(normalizedData);
return saveUserToDatabase(normalizedData);
}
// ❌ Плохо: мутация данных
function addTodoItem(todos, newItem) {
todos.push(newItem); // Изменяет исходный массив
return todos;
}
// ✅ Хорошо: иммутабельное добавление
function addTodoItem(todos, newItem) {
return [...todos, newItem]; // Возвращает новый массив
}
// ❌ Плохо: мутация объекта
function updateUserProfile(user, updates) {
user.name = updates.name; // Изменяет исходный объект
user.updatedAt = Date.now();
return user;
}
// ✅ Хорошо: иммутабельное обновление
function updateUserProfile(user, updates) {
return {
...user,
...updates,
updatedAt: Date.now()
};
}
// ❌ Плохо: скрытая зависимость от глобальной переменной
const TAX_RATE = 0.18;
function calculateTotalPrice(price) {
return price * (1 + TAX_RATE); // Зависит от глобальной переменной
}
// ✅ Хорошо: явная передача зависимостей
function calculateTotalPrice(price, taxRate) {
return price * (1 + taxRate);
}
// Или с значением по умолчанию
function calculateTotalPrice(price, taxRate = 0.18) {
return price * (1 + taxRate);
}
/**
* Вычисляет итоговую стоимость с учётом скидки и налога
* @pure
* @param {number} price - Базовая цена
* @param {number} discountPercent - Процент скидки (0-100)
* @param {number} taxRate - Налоговая ставка (0-1)
* @returns {number} Итоговая стоимость
*/
function calculateFinalPrice(price, discountPercent, taxRate) {
const discountAmount = price * (discountPercent / 100);
const discountedPrice = price - discountAmount;
return discountedPrice * (1 + taxRate);
}
/**
* Форматирует имя пользователя
* @pure
* @param {string} firstName - Имя
* @param {string} lastName - Фамилия
* @returns {string} Отформатированное полное имя
*/
function formatUserName(firstName, lastName) {
return `${firstName.trim()} ${lastName.trim()}`;
}
❌ Проблема: Функция кажется чистой, но изменяет входные данные
// Кажется чистой, но мутирует массив
function sortUsers(users) {
return users.sort((a, b) => a.name.localeCompare(b.name));
}
// Array.sort() изменяет исходный массив!
const originalUsers = [{ name: 'Боб' }, { name: 'Анна' }];
const sortedUsers = sortUsers(originalUsers);
console.log(originalUsers); // Тоже отсортирован!
✅ Решение: Создавайте копию перед мутирующими операциями
function sortUsers(users) {
return [...users].sort((a, b) => a.name.localeCompare(b.name));
}
// ❌ Плохо: зависит от текущего времени
function createTimestamp() {
return Date.now();
}
function isWorkingHours() {
const hour = new Date().getHours();
return hour >= 9 && hour <= 18;
}
// ✅ Хорошо: принимает время как параметр
function createTimestamp(date = new Date()) {
return date.getTime();
}
function isWorkingHours(date = new Date()) {
const hour = date.getHours();
return hour >= 9 && hour <= 18;
}
// ❌ Плохо: скрытое логирование
function calculateDiscount(price, percent) {
console.log(`Расчёт скидки: ${price} * ${percent}%`); // Побочный эффект!
return price * (percent / 100);
}
// ✅ Хорошо: чистая функция + отдельное логирование
function calculateDiscount(price, percent) {
return price * (percent / 100);
}
function calculateDiscountWithLogging(price, percent) {
const result = calculateDiscount(price, percent);
console.log(`Расчёт скидки: ${price} * ${percent}% = ${result}`);
return result;
}
// Чистая функция
function validatePassword(password) {
const errors = [];
if (password.length < 8) {
errors.push('Минимум 8 символов');
}
if (!/[A-Z]/.test(password)) {
errors.push('Нужна заглавная буква');
}
if (!/[0-9]/.test(password)) {
errors.push('Нужна цифра');
}
return {
isValid: errors.length === 0,
errors
};
}
// Простые тесты
function testValidatePassword() {
// Тест валидного пароля
const validResult = validatePassword('Password123');
console.assert(validResult.isValid === true);
console.assert(validResult.errors.length === 0);
// Тест короткого пароля
const shortResult = validatePassword('Pass1');
console.assert(shortResult.isValid === false);
console.assert(shortResult.errors.includes('Минимум 8 символов'));
// Тест без заглавной буквы
const noUpperResult = validatePassword('password123');
console.assert(noUpperResult.isValid === false);
console.assert(noUpperResult.errors.includes('Нужна заглавная буква'));
// Тест без цифры
const noDigitResult = validatePassword('Password');
console.assert(noDigitResult.isValid === false);
console.assert(noDigitResult.errors.includes('Нужна цифра'));
console.log('Все тесты пройдены!');
}
testValidatePassword();
// Тестирование свойств чистых функций
function testMathProperties() {
// Коммутативность сложения
for (let i = 0; i < 100; i++) {
const a = Math.random() * 1000;
const b = Math.random() * 1000;
console.assert(add(a, b) === add(b, a));
}
// Ассоциативность сложения
for (let i = 0; i < 100; i++) {
const a = Math.random() * 1000;
const b = Math.random() * 1000;
const c = Math.random() * 1000;
console.assert(add(add(a, b), c) === add(a, add(b, c)));
}
console.log('Математические свойства проверены!');
}
function add(a, b) {
return a + b;
}
Помните: чистые функции — это не просто хорошая практика, это фундамент качественного, поддерживаемого и надёжного кода. Стремитесь к чистоте функций везде, где это возможно, и изолируйте побочные эффекты в отдельных функциях.
Хотите больше статей по подготовке к собеседованиям?
Подписывайтесь на EasyAdvice (@AleksandrEmolov_EasyAdvice), добавляйте сайт в закладки и прокачивайтесь каждый день 💪