Функциональное программирование
Функциональное программирование (ФП) — это парадигма, в которой вычисления строятся из функций, а не из изменяемого состояния. JavaScript не является чисто функциональным языком, но заимствует у ФП много полезных техник: чистые функции, иммутабельность, методы массивов, каррирование, композицию. Эти приёмы делают код предсказуемее, проще для тестирования и удобнее для поддержки.
Содержание
- Чистые функции
- Иммутабельность
- Методы массивов
- Каррирование и частичное применение
- Композиция функций
debounceиthrottle- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Чистые функции
Чистая функция — это функция, которая:
- Для одних и тех же аргументов всегда возвращает один и тот же результат.
- Не имеет побочных эффектов: не изменяет внешнее состояние, не делает сетевых запросов, не пишет в DOM, не использует
Math.random()и текущее время.
// чистая функция
function add(a, b) {
return a + b;
}
// нечистая функция: зависит от внешнего состояния
let tax = 0.2;
function calculatePrice(price) {
return price * (1 + tax);
}
// нечистая функция: побочный эффект
function logAndDouble(x) {
console.log(x);
return x * 2;
}
Преимущества чистых функций:
- их легко тестировать — достаточно проверить вход и выход;
- их легко распараллеливать и кэшировать (memoization);
- их поведение предсказуемо и не зависит от порядка вызовов.
В реальном коде полностью избежать побочных эффектов невозможно: рендеринг, запросы к серверу, работа с DOM — всё это эффекты. Важно изолировать их и не смешивать с вычислениями.
Иммутабельность
Иммутабельность — это отказ от изменения существующих данных. Вместо изменения объекта или массива создаётся новый объект на основе старого.
const numbers = [1, 2, 3];
// мутация исходного массива
numbers.push(4);
// иммутабельный подход
const nextNumbers = [...numbers, 4];
Почему это важно:
- иммутабельные данные безопаснее в многопоточных/асинхронных сценариях;
- их удобно сравнивать по ссылке (React использует это для
React.memoиuseEffect); - история изменений сохраняется автоматически.
const user = { name: 'Alice', age: 25 };
// неправильно: мутация
user.age = 26;
// правильно: новый объект
const updatedUser = { ...user, age: 26 };
Для глубокой иммутабельности и сложных структур используют библиотеки вроде Immer или Immutable.js. В простых случаях хватает spread, map, filter и reduce.
Методы массивов
Методы массивов в JavaScript — основной инструмент функционального стиля. Они не мутируют исходный массив, а возвращают новое значение.
map
Преобразует каждый элемент массива и возвращает новый массив той же длины.
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6]
console.log(numbers); // [1, 2, 3]
filter
Возвращает новый массив, оставляя только элементы, прошедшие проверку.
const numbers = [1, 2, 3, 4, 5];
const evens = numbers.filter((n) => n % 2 === 0);
console.log(evens); // [2, 4]
reduce
Сворачивает массив к одному значению. Принимает аккумулятор и текущий элемент.
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 10
reduce можно использовать не только для чисел:
const people = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
];
const byName = people.reduce((acc, person) => {
acc[person.name] = person;
return acc;
}, {});
console.log(byName);
// { Alice: { name: 'Alice', age: 25 }, Bob: { name: 'Bob', age: 30 } }
find, some, every
const users = [
{ name: 'Alice', admin: true },
{ name: 'Bob', admin: false },
];
const admin = users.find((u) => u.admin);
console.log(admin.name); // Alice
const hasAdmins = users.some((u) => u.admin);
console.log(hasAdmins); // true
const allAdmins = users.every((u) => u.admin);
console.log(allAdmins); // false
Сравнение методов
| Метод | Что возвращает | Когда использовать |
|---|---|---|
map | Новый массив той же длины | Преобразовать каждый элемент |
filter | Новый массив, меньшей или равной длины | Оставить элементы по условию |
reduce | Одно значение | Свернуть массив: сумма, объект, группировка |
find | Первый подходящий элемент или undefined | Найти один элемент |
some | true, если хотя бы один подходит | Проверить наличие |
every | true, если все подходят | Проверить все |
Каррирование и частичное применение
Каррирование — преобразование функции от нескольких аргументов в цепочку функций от одного аргумента.
function add(a) {
return function (b) {
return a + b;
};
}
const addFive = add(5);
console.log(addFive(3)); // 8
console.log(add(2)(3)); // 5
В современном синтаксисе:
const multiply = (a) => (b) => a * b;
const double = multiply(2);
console.log(double(7)); // 14
Частичное применение — фиксация части аргументов функции заранее.
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = greet.bind(null, 'Hello');
console.log(sayHello('Alice')); // Hello, Alice!
Или через стрелки:
const greet = (greeting) => (name) => `${greeting}, ${name}!`;
const sayHi = greet('Hi');
console.log(sayHi('Bob')); // Hi, Bob!
Каррирование полезно, когда нужно создавать специализированные функции на основе общих.
Композиция функций
Композиция — объединение нескольких простых функций в одну, где результат одной передаётся в следующую.
const trim = (s) => s.trim();
const toUpper = (s) => s.toUpperCase();
const exclaim = (s) => `${s}!`;
const shout = (s) => exclaim(toUpper(trim(s)));
console.log(shout(' hello ')); // HELLO!
Для удобства пишут универсальную функцию композиции:
const compose = (...fns) => (x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
const shout = compose(exclaim, toUpper, trim);
console.log(shout(' hello ')); // HELLO!
Если читать функции слева направо, используют pipe:
const pipe = (...fns) => (x) =>
fns.reduce((acc, fn) => fn(acc), x);
const shout = pipe(trim, toUpper, exclaim);
console.log(shout(' hello ')); // HELLO!
Композиция помогает строить сложную логику из маленьких независимых функций.
debounce и throttle
Эти два приёма ограничивают частоту вызова функций — например, при обработке событий scroll, resize или input.
debounce
Debounce откладывает выполнение функции до тех пор, пока не пройдёт заданное время с момента последнего вызова.
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => fn.apply(this, args), delay);
};
}
const handleInput = debounce((value) => {
console.log('Search:', value);
}, 300);
// handleInput('a');
// handleInput('ab');
// handleInput('abc'); // выполнится только этот вызов, спустя 300 мс после него
Применение: поиск при вводе текста, валидация формы после окончания ввода.
throttle
Throttle гарантирует, что функция выполняется не чаще, чем раз в заданный интервал.
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 200);
window.addEventListener('scroll', handleScroll);
Применение: обработка скролла, ресайза, отслеживание координат мыши.
Сравнение
| Приём | Когда вызывает функцию | Пример использования |
|---|---|---|
debounce | После паузы между вызовами | Поиск по вводу |
throttle | Регулярно, не чаще интервала | Обработка скролла |
Ключевые тезисы для интервью
- Чистая функция всегда возвращает одинаковый результат для одинаковых аргументов и не имеет побочных эффектов.
- Иммутабельность важна, потому что React использует сравнение по ссылке для оптимизации рендеринга (
React.memo,useEffect). reduceсворачивает массив к одному значению и подходит для сумм, группировки, трансформации в объекты.mapвозвращает новый массив той же длины, аforEachничего не возвращает и мутирует через побочные эффекты.- Каррирование превращает
f(a, b)вf(a)(b), частичное применение фиксирует часть аргументов черезbindили замыкание. composeвыполняет функции справа налево,pipe— слева направо; результат одинаковый, направление чтения разное.debounceоткладывает вызов до паузы между событиями,throttleгарантирует регулярный вызов не чаще заданного интервала.- Иммутабельное обновление объекта:
{ ...obj, key: newValue }, массива:[...arr, newItem]илиarr.map(...).
Заключение
Функциональное программирование в JavaScript — это набор практик, которые делают код предсказуемым и удобным для тестирования. Чистые функции без побочных эффектов, иммутабельные данные, методы массивов map/filter/reduce и композиция функций — основные инструменты ФП в повседневной разработке. debounce и throttle решают проблему частых вызовов при обработке событий. Применяйте эти приёмы осознанно: не нужно переписывать весь код в функциональном стиле, но ключевые идеи значительно улучшают качество кода.