Функциональное программирование в JavaScript: от теории к практике
Функциональное программирование (ФП) — это парадигма, которая рассматривает вычисления как вычисление математических функций и избегает изменения состояния и изменяемых данных. В современном JavaScript, особенно в среде Node.js, функциональный подход становится всё более популярным благодаря своей предсказуемости, тестируемости и модульности. В этой статье мы разберем ключевые концепции ФП и покажем, как применять их в реальных проектах.
1. Чистые функции и отсутствие побочных эффектов
Чистая функция — это функция, которая:
- Всегда возвращает один и тот же результат для одних и тех же аргументов (детерминизм).
- Не вызывает побочных эффектов (не изменяет внешние переменные, не выводит данные, не обращается к файловой системе).
Пример чистой функции:
// Чистая функция: складывает два числа
const add = (a, b) => a + b;
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 5 — всегда одинаково
Пример нечистой функции:
let counter = 0;
const increment = () => {
counter++; // Побочный эффект: изменяет внешнюю переменную
return counter;
};
console.log(increment()); // 1
console.log(increment()); // 2 — результат разный
В функциональном программировании мы стремимся писать только чистые функции. Это делает код легче для тестирования и отладки.
2. Иммутабельность данных
Иммутабельность означает, что данные не изменяются после создания. Вместо изменения существующего объекта мы создаём его новую копию с нужными изменениями. В JavaScript для этого часто используют оператор spread (...) или методы Object.assign.
Пример иммутабельного обновления объекта:
const user = { name: 'Alice', age: 30 };
// Плохо: мутация
user.age = 31;
// Хорошо: создаём новый объект
const updatedUser = { ...user, age: 31 };
console.log(user); // { name: 'Alice', age: 30 }
console.log(updatedUser); // { name: 'Alice', age: 31 }
Для массивов также используем иммутабельные методы:
const numbers = [1, 2, 3];
// Мутация (плохо)
numbers.push(4);
// Иммутабельно (хорошо)
const newNumbers = [...numbers, 4];
console.log(numbers); // [1, 2, 3]
console.log(newNumbers); // [1, 2, 3, 4]
3. Функции высшего порядка: map, filter, reduce
Функции высшего порядка — это функции, которые принимают другие функции в качестве аргументов или возвращают их. Три самых популярных метода в JavaScript — map, filter и reduce. Они позволяют обрабатывать массивы без циклов и мутаций.
3.1. map() — преобразование массива
Метод map создаёт новый массив, применяя функцию к каждому элементу исходного массива.
const prices = [100, 200, 300];
const withTax = prices.map(price => price * 1.2);
console.log(withTax); // [120, 240, 360]
// Исходный массив не изменился
console.log(prices); // [100, 200, 300]
3.2. filter() — фильтрация массива
Метод filter возвращает новый массив, содержащий только те элементы, для которых переданная функция вернула true.
const ages = [15, 22, 18, 30, 12];
const adults = ages.filter(age => age >= 18);
console.log(adults); // [22, 18, 30]
3.3. reduce() — агрегация данных
Метод reduce сводит массив к одному значению (числу, объекту, строке).
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, current) => accumulator + current, 0);
console.log(sum); // 15
Комбинируя эти методы, можно решать сложные задачи в функциональном стиле:
const users = [
{ name: 'Alice', age: 25, active: true },
{ name: 'Bob', age: 17, active: false },
{ name: 'Charlie', age: 30, active: true },
];
// Получить имена активных пользователей старше 18 лет
const result = users
.filter(user => user.active && user.age >= 18)
.map(user => user.name);
console.log(result); // ['Alice', 'Charlie']
4. Замыкания и каррирование
Замыкание — это функция, которая запоминает своё лексическое окружение, даже если она выполняется вне своей области видимости. Это основа для создания функций с "памятью" и для каррирования.
Пример замыкания:
const createCounter = () => {
let count = 0;
return () => {
count++;
return count;
};
};
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Каррирование — это преобразование функции от многих аргументов в последовательность функций от одного аргумента. Это упрощает композицию и частичное применение.
// Обычная функция
const multiply = (a, b) => a * b;
// Каррированная версия
const curriedMultiply = (a) => (b) => a * b;
const double = curriedMultiply(2);
console.log(double(5)); // 10
const triple = curriedMultiply(3);
console.log(triple(5)); // 15
Каррирование особенно полезно при работе с функциями высшего порядка и конфигурацией поведения.
5. Композиция функций
Композиция — это объединение нескольких функций в одну, где выход одной функции становится входом для следующей. Это позволяет строить сложные операции из простых, переиспользуемых блоков.
Простая реализация композиции:
const compose = (f, g) => (x) => f(g(x));
const addOne = (x) => x + 1;
const double = (x) => x * 2;
const addOneThenDouble = compose(double, addOne);
console.log(addOneThenDouble(5)); // (5 + 1) * 2 = 12