Что такое чистая функция в JavaScript?

👨‍💻 Frontend Developer 🟡 Часто попадается 🎚️ Легкий
#JavaScript #База JS #функции

Краткий ответ

Чистая функция — это функция, которая:

  1. Всегда возвращает одинаковый результат для одинаковых входных данных
  2. Не имеет побочных эффектов (не изменяет внешнее состояние)
  3. Не зависит от внешнего состояния (только от своих параметров)
// Чистая функция
function add(a, b) {
  return a + b; // Всегда одинаковый результат для одинаковых a и b
}
 
// Нечистая функция
let counter = 0;
function increment() {
  return ++counter; // Зависит от внешней переменной
}

Что такое чистая функция

Чистая функция (Pure Function) — это фундаментальная концепция функционального программирования, которая определяет функцию как “математическую” операцию. Такие функции являются основой предсказуемого и надежного кода.

Основные принципы чистых функций

  1. Детерминированность — одинаковые входные данные всегда дают одинаковый результат
  2. Отсутствие побочных эффектов — функция не изменяет ничего за пределами своей области видимости
  3. Ссылочная прозрачность — вызов функции можно заменить её результатом без изменения поведения программы

Детерминированность: одинаковый вход — одинаковый выход

✅ Примеры чистых функций

// Математические операции
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;
}

Отсутствие побочных эффектов

Что такое побочные эффекты

Побочные эффекты — это любые изменения состояния программы или взаимодействие с внешним миром:

  • Изменение глобальных переменных
  • Модификация переданных объектов/массивов
  • Вывод в консоль или на экран
  • Запросы к серверу
  • Изменение DOM
  • Запись в файлы
  • Изменение состояния базы данных

✅ Функции без побочных эффектов

// Возвращает новый массив, не изменяя исходный
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;
}

Преимущества чистых функций

1. Предсказуемость и надёжность

// Чистая функция - всегда предсказуемый результат
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 ₽" - всегда одинаково

2. Лёгкость тестирования

// Чистую функцию легко тестировать
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('Скидка должна быть'));
  }
}

3. Возможность мемоизации

// Чистые функции можно мемоизировать
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)); // Берётся из кэша

4. Параллельное выполнение

// Чистые функции безопасны для параллельного выполнения
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);
}

Сравнение: чистые vs нечистые функции

АспектЧистые функцииНечистые функции
ПредсказуемостьВсегда одинаковый результатРезультат может меняться
ТестированиеЛегко тестироватьТребуют моков и заглушек
ОтладкаПростая отладкаСложная отладка
КэшированиеМожно мемоизироватьНельзя кэшировать
ПараллелизмБезопасны для параллельного выполненияМогут вызывать race conditions
РефакторингЛегко рефакторитьСложно рефакторить
Понимание кодаЛегко понять логикуНужно учитывать контекст
ПереиспользованиеВысокая переиспользуемостьОграниченная переиспользуемость

Лучшие практики

1. Стремитесь к чистоте

// ❌ Плохо: смешивание чистой логики и побочных эффектов
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);
}

2. Используйте иммутабельность

// ❌ Плохо: мутация данных
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()
  };
}

3. Избегайте скрытых зависимостей

// ❌ Плохо: скрытая зависимость от глобальной переменной
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);
}

4. Документируйте чистоту функций

/**
 * Вычисляет итоговую стоимость с учётом скидки и налога
 * @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()}`;
}

Частые ошибки

1. Скрытая мутация

Проблема: Функция кажется чистой, но изменяет входные данные

// Кажется чистой, но мутирует массив
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));
}

2. Зависимость от времени

// ❌ Плохо: зависит от текущего времени
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;
}

3. Скрытые побочные эффекты

// ❌ Плохо: скрытое логирование
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();

Property-based тестирование

// Тестирование свойств чистых функций
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;
}

Заключение

Ключевые преимущества чистых функций

  1. Предсказуемость — одинаковые входные данные всегда дают одинаковый результат
  2. Тестируемость — легко писать и поддерживать тесты
  3. Отладка — проще найти и исправить ошибки
  4. Переиспользование — можно использовать в разных контекстах
  5. Параллелизм — безопасны для многопоточного выполнения
  6. Оптимизация — возможность мемоизации и других оптимизаций

Когда использовать чистые функции

  • Всегда, когда это возможно
  • Для бизнес-логики и вычислений
  • При обработке данных
  • Для валидации и форматирования
  • В утилитарных функциях
  • При работе с иммутабельными структурами данных

Современные тенденции

  • Функциональное программирование становится популярнее
  • Иммутабельность как основа надёжного кода
  • Redux и другие библиотеки состояния основаны на чистых функциях
  • React поощряет использование чистых компонентов
  • TypeScript помогает обеспечить типобезопасность чистых функций

Помните: чистые функции — это не просто хорошая практика, это фундамент качественного, поддерживаемого и надёжного кода. Стремитесь к чистоте функций везде, где это возможно, и изолируйте побочные эффекты в отдельных функциях.


Хотите больше статей по подготовке к собеседованиям?
Подписывайтесь на EasyAdvice (@AleksandrEmolov_EasyAdvice), добавляйте сайт в закладки и прокачивайтесь каждый день 💪