Навигация

Функциональное программирование

Функциональное программирование (ФП) — это парадигма, в которой вычисления строятся из функций, а не из изменяемого состояния. JavaScript не является чисто функциональным языком, но заимствует у ФП много полезных техник: чистые функции, иммутабельность, методы массивов, каррирование, композицию. Эти приёмы делают код предсказуемее, проще для тестирования и удобнее для поддержки.

Содержание

  1. Чистые функции
  2. Иммутабельность
  3. Методы массивов
  4. Каррирование и частичное применение
  5. Композиция функций
  6. debounce и throttle
  7. Ключевые тезисы для интервью
  8. Заключение
  9. Полезные ссылки

Чистые функции

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

  1. Для одних и тех же аргументов всегда возвращает один и тот же результат.
  2. Не имеет побочных эффектов: не изменяет внешнее состояние, не делает сетевых запросов, не пишет в 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Найти один элемент
sometrue, если хотя бы один подходитПроверить наличие
everytrue, если все подходятПроверить все

Каррирование и частичное применение

Каррирование — преобразование функции от нескольких аргументов в цепочку функций от одного аргумента.

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 решают проблему частых вызовов при обработке событий. Применяйте эти приёмы осознанно: не нужно переписывать весь код в функциональном стиле, но ключевые идеи значительно улучшают качество кода.

Полезные ссылки