Чистые функции в JavaScript: понятие, преимущества и практическое руководство

Чистые функции — это функции, которые не производят побочных эффектов и при одинаковых входных параметрах всегда возвращают одинаковый результат.
Их применяют, чтобы код был аккуратным, поддерживаемым и легко тестируемым. Чистые функции предсказуемы и не изменяют внешнее состояние. Они проще для отладки и полезны при разработке сложных систем. Ниже разберём, как распознать чистую функцию в JavaScript, какие у неё характеристики и какие преимущества она даёт.
Что такое чистая функция
Определение простое: чистая функция — это функция, у которой есть только вход и выход, и она не взаимодействует с чем‑то снаружи. Если дать одной и той же функции одинаковые аргументы, она вернёт один и тот же результат сколько угодно раз.
Ключевые моменты:
- Входы → Выход без побочных эффектов.
- Не зависит от внешнего состояния (глобальных переменных, времени, случайности и т. п.).
- Не изменяет переданные объекты (не мутирует аргументы).
Характеристики чистой функции
Постоянный результат
Чистая функция всегда возвращает одно и то же значение для одних и тех же аргументов.
Пример:
function multiply(a, b) {
return a * b;
}
Функция multiply выше всегда вернёт произведение двух аргументов. Повторный вызов с теми же аргументами даст тот же результат:
multiply(2, 3); // returns 6
multiply(2, 3); // returns 6
multiply(2, 3); // returns 6
Контрпример с непредсказуемым поведением:
function multiplyRandomNumber(num) {
return num * Math.floor(Math.random() * 10);
}
multiplyRandomNumber(5); // Unpredictable results
multiplyRandomNumber(5); // Unpredictable results
multiplyRandomNumber(5); // Unpredictable results
multiplyRandomNumber зависит от Math.random(), поэтому результат меняется — это императивный признак нечистой функции.
Отсутствие побочных эффектов
Чистая функция не должна изменять состояние вне своего лексического окружения. Побочный эффект — любое действие, которое наблюдается за пределами функции: изменение глобальной переменной, запись в консоль, сетевой запрос, модификация DOM и т. п.
Нечистый пример:
let count = 0;
function increment() {
count++;
console.log(count);
}
increment(); // Logs 1
increment(); // Logs 2
increment(); // Logs 3
Функция increment меняет внешний count и выводит в консоль — она нечистая.
Приведём чистую версию:
function increment(count) {
return count + 1;
}
increment(1); // returns 2
increment(1); // returns 2
increment(1); // returns 2
Эта версия не меняет внешний state и при одинаковом входе возвращает одинаковое значение.
Другие правила
- Функция не должна модифицировать свои аргументы. Если нужно изменить структуру — создайте копию и изменяйте её.
- Функция должна возвращать значение. Если у неё нет return и нет побочных эффектов, она бесполезна.
- Не полагайтесь на внешнее состояние. Все необходимые данные должны приходить через параметры.
Важно: «чистота» — это свойство кода на уровне функциональных блоков. В реальном приложении часть функций будет нечистой (ввод/вывод, работа с сетью, логирование). Хорошая архитектура отделяет чистую бизнес‑логику от нечистой инфраструктуры.
Преимущества чистых функций
Тестируемость
Чистые функции легко тестировать: вход → ожидаемый выход. Нет необходимости поднимать сложный контекст или мокать внешнее состояние.
Пример: для multiply можно писать простые модульные тесты без подготовки окружения.
Мемоизация
Поскольку чистая функция при одинаковом входе даёт одинаковый выход и не имеет побочных эффектов, её легко кешировать. Мемоизация подходит для дорогих вычислений и повторяющихся запросов с теми же данными.
Подсказка: мемоизация не годится для нечистых функций — кэш даст устаревший результат.
Безопасность при параллельном выполнении
Чистые функции не изменяют общий state, следовательно, при параллельном выполнении они не создают гонок и не требуют синхронизации. Это упрощает масштабирование и использование воркеров.
Простая отладка и предсказуемость
Если функция чиста, локализовать причину ошибки проще: достаточно проверить входные данные и функцию, не анализируя глобальное состояние.
Когда чистые функции не подходят
- Взаимодействие с внешними системами: сетевые запросы, чтение/запись в БД, доступ к файловой системе — эти операции сами по себе нечисты.
- Обновление приложения: UI, хранение состояния сессии, логирование — эти задачи требуют побочных эффектов.
- Высокая накладная стоимость копирования больших объектных графов. Иногда мутация с контролем (например, с копированием «по необходимости» или использованием структур неизменяемых данных) эффективнее.
Важно: цель — не писать всё чистым, а изолировать чистую бизнес‑логику от нечистой инфраструктуры.
Как превратить нечистую функцию в чистую: мини‑методика
- Выявите внешние зависимости и побочные эффекты.
- Вынесите чтение/запись в отдельные слои (слой ввода/вывода).
- Передавайте все необходимые данные через аргументы.
- Не мутируйте входные структуры — создавайте копии или используйте неизменяемые структуры.
- Всегда возвращайте значение, даже если это объект с метаданными об ошибке.
Пример рефакторинга:
- Нечистая версия делает запрос к API и обновляет глобальный state.
- Рефакторинг: функция получает данные и возвращает результат обработки; сетевой вызов и запись в state выполняются снаружи, с использованием результата чистой функции.
Паттерны и альтернативы
- Иммутабельность: используйте копии данных или библиотеки (immer) для удобной работы с неизменяемыми структурами.
- Dependency injection: передавайте функции зависимости (например, генератор случайных чисел) как параметры, чтобы можно было подменять их в тестах.
- Чистая оболочка (pure wrapper): оборачивайте нечистые операции в чистую функцию, возвращающую промис/результат, а сам побочный эффект выполняйте отдельным слоем.
Ментальные модели для принятия решений
- «Входы в коробке»: представьте функцию как коробку: все, что ей нужно, должно быть в аргументах; она возвращает результат наружу и ничего не трогает по пути.
- «Отделение мира»: логика превращается в чистые функции; взаимодействие с миром выполняют адаптеры (I/O). Это шаблон портов и адаптеров (hexagonal architecture).
Контрпримеры и характерные ошибки
- Использование Date.now() внутри функции для вычисления поведения — делает функцию нечистой.
- Модификация аргумента объекта внутри функции. Даже если тесты проходят, такое поведение ломает инварианты и может вызвать баги.
Чек‑лист для команды
Разработчик:
- Все входные данные передаются через параметры.
- Нет модификации глобальных переменных.
- Не мутируются переданные объекты.
- Есть return с ожидаемой структурой.
Код‑ревьювер:
- Проверить зависимости функции.
- Проверить, используются ли внешние сервисы внутри функции.
- Проверить тесты на повторяемость и детерминированность.
Тестер:
- Написать позитивные и негативные тесты для входов и граничных случаев.
- Проверить поведение при одинаковых входах несколько раз.
- Отдельно тестировать интеграцию с адаптерами ввода/вывода.
Критерии приёмки
- Функция возвращает детерминированный результат для заданного набора входных данных.
- Нет побочных эффектов (изменение глобальных переменных, console.log, сетевые вызовы) внутри функции.
- Входные структуры не мутируются (тесты проверяют исходные объекты до и после вызова).
Тест‑кейсы для примера multiply
- multiply(0, 5) → 0.
- multiply(-1, 5) → -5.
- multiply(2, 3) повторно → всегда 6.
- Передать нечисловой аргумент → выбрасывает ошибку или возвращает NaN, ожидаемое поведение документировано.
Безопасность и приватность
Чистые функции облегчают безопасность, потому что они не осуществляют прямых операций ввода/вывода. Данные не утекают из функции, если вы явно не возвращаете их. Однако безопасность всё равно зависит от кода, который вызывает функцию и от того, как обрабатываются результаты.
Быстрая таблица сравнения
| Свойство | Чистая функция | Нечистая функция |
|---|---|---|
| Детерминированность | Да | Нет |
| Тестируемость | Легко | Сложно |
| Параллелизация | Простая | Требует синхронизации |
| Подходит для I/O | Нет | Да |
Пример решения зависимости от случайности через внедрение зависимостей
function multiplyWithRandom(num, rng) {
// rng передаётся извне, в тестах можно подменить
return num * Math.floor(rng() * 10);
}
// В проде
multiplyWithRandom(5, Math.random);
// В тесте
multiplyWithRandom(5, () => 0.5); // детерминированно
Decision flowchart
flowchart TD
A[Есть ли побочные эффекты?] -->|Да| B[Вынести побочные эффекты в адаптер]
A -->|Нет| C[Проверить зависимость от внешнего состояния]
C -->|Зависит| B
C -->|Не зависит| D[Функция чиста — можно мемоизировать и параллелить]Краткий глоссарий
- Побочный эффект: любое наблюдаемое изменение состояния вне функции.
- Мемоизация: кеширование результатов функции для ускорения повторных вызовов.
- Иммутабельность: свойство данных не изменяться после создания.
Итог
Чистые функции — мощный инструмент для создания предсказуемой, тестируемой и масштабируемой логики. Они не заменяют необходимость в операциях ввода/вывода, но помогают изолировать бизнес‑логику и снизить сложность системы.
Важно: не стремитесь делать всё чистым любой ценой. Стратегия, которая работает лучше всего, — это отделять чистую логику от нечистых адаптеров и последовательно рефакторить критичные части приложения.
Важно: при рефакторинге проверяйте производительность и читаемость. Иногда контролируемая мутация в локальном контексте может быть оправдана.
Применяйте чек‑листы и методику, описанную выше, чтобы переводить функции в чистый вид там, где это даёт максимум выгоды.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone