Функциональное программирование в JavaScript: от А до Я
Функциональное программирование (ФП) — это парадигма, которая рассматривает вычисления как вычисление математических функций и избегает изменения состояния и изменяемых данных. В контексте JavaScript (Node.js) ФП становится мощным инструментом для создания предсказуемого, тестируемого и масштабируемого кода. В этой статье мы разберем ключевые концепции ФП от основ до продвинутых техник, подкрепляя теорию практическими примерами.
Основы: Чистые функции и иммутабельность
В основе ФП лежат два принципа: чистые функции и иммутабельность (неизменяемость данных).
Чистая функция всегда возвращает одинаковый результат для одних и тех же аргументов и не имеет побочных эффектов (не изменяет внешние переменные, не выводит в консоль, не делает запросы к API).
// Чистая функция
const add = (a, b) => a + b;
// Нечистая функция (побочный эффект — изменение внешней переменной)
let counter = 0;
const increment = () => counter++;
Иммутабельность означает, что данные не изменяются напрямую, а создаются их новые копии с нужными изменениями. В JavaScript для этого используются методы, возвращающие новые объекты/массивы.
// Иммутабельное обновление объекта
const user = { name: 'Alice', age: 30 };
const updatedUser = { ...user, age: 31 }; // Создаем новый объект
// Иммутабельное обновление массива
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4]; // [1, 2, 3, 4]
Замыкания и функции высшего порядка
Замыкание — это функция, которая "запоминает" свое лексическое окружение даже после того, как внешняя функция завершила выполнение. Это основа для многих паттернов ФП.
const createMultiplier = (factor) => {
// Внутренняя функция — замыкание
return (number) => number * factor;
};
const double = createMultiplier(2);
console.log(double(5)); // 10
Функции высшего порядка — это функции, которые принимают другие функции как аргументы или возвращают их. Классические примеры — map, filter, reduce.
const numbers = [1, 2, 3, 4, 5];
// map — преобразует каждый элемент
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]
// filter — отбирает элементы по условию
const even = numbers.filter(n => n % 2 === 0); // [2, 4]
// reduce — сворачивает массив в одно значение
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15
Композиция функций и каррирование
Композиция функций — это объединение нескольких функций в одну, где результат одной функции передается на вход следующей. Это позволяет строить сложные операции из простых, переиспользуемых блоков.
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const trim = (str) => str.trim();
const toLowerCase = (str) => str.toLowerCase();
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const processName = compose(capitalize, toLowerCase, trim);
console.log(processName(' JOHN DOE ')); // 'John doe'
Каррирование — это преобразование функции с несколькими аргументами в последовательность функций, каждая из которых принимает один аргумент. Это упрощает частичное применение и композицию.
// Обычная функция
const multiply = (a, b, c) => a * b * c;
// Каррированная версия
const curriedMultiply = (a) => (b) => (c) => a * b * c;
console.log(curriedMultiply(2)(3)(4)); // 24
// Частичное применение
const multiplyBy2 = curriedMultiply(2);
const multiplyBy2And3 = multiplyBy2(3);
console.log(multiplyBy2And3(5)); // 30
Практический пример: обработка данных в Node.js
Рассмотрим реальный сценарий: у нас есть массив заказов из интернет-магазина, и нужно получить сумму всех оплаченных заказов, сделанных в 2024 году, с учетом скидки.
const orders = [
{ id: 1, amount: 100, status: 'paid', date: '2024-05-10', discount: 0.1 },
{ id: 2, amount: 200, status: 'pending', date: '2024-06-15', discount: 0 },
{ id: 3, amount: 150, status: 'paid', date: '2023-12-20', discount: 0.2 },
{ id: 4, amount: 300, status: 'paid', date: '2024-01-05', discount: 0.15 },
];
// Функциональный подход
const isPaid = (order) => order.status === 'paid';
const isIn2024 = (order) => new Date(order.date).getFullYear() === 2024;
const applyDiscount = (order) => order.amount * (1 - order.discount);
const sum = (acc, value) => acc + value;
const totalRevenue = orders
.filter(isPaid) // Оставляем только оплаченные
.filter(isIn2024) // Оставляем только за 2024 год
.map(applyDiscount) // Применяем скидку
.reduce(sum, 0); // Суммируем
console.log(totalRevenue); // 355 (100*0.9 + 300*0.85)
Каждая функция здесь — чистая и делает только одно действие. Это делает код легко читаемым, тестируемым и расширяемым. Если нужно добавить новое условие (например, фильтр по минимальной сумме), достаточно добавить еще один .filter().
Заключение
Функциональное программирование в JavaScript (Node.js) — это не просто модный тренд, а мощный инструмент для написания надежного и предсказуемого кода. Освоив чистые функции, иммутабельность, замыкания, композицию и каррирование, вы сможете:
- Уменьшить количество багов за счет отсутствия побочных эффектов.
- Упростить тестирование — чистые функции не требуют моков или сложной настройки окружения.
- Повысить читаемость кода — декларативный стиль лучше описы