Перейти к содержанию
Educora
Продвинутый22 мин19 / 27

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

Пиши предсказуемый код: чистые функции, неизменяемость, функции высшего порядка, каррирование, композиция и мощные приёмы с `reduce`.

Проверь себя
В этом уроке ты узнаешь
  • Отличать чистые функции от нечистых, избегать побочных эффектов и изменения данных
  • Обновлять массивы и объекты без изменения исходных: spread, map, toSorted, structuredClone
  • Строить маленькие переиспользуемые функции с помощью функций высшего порядка, каррирования и композиции (pipe/compose)
  • Использовать reduce для группировки, подсчёта и индексации данных

Приходит жалоба: «Сумма в корзине иногда неверная». Час поисков — и находишь функцию, которая молча изменила переданный ей массив. Функциональное программирование (ФП) — стиль, в котором такие ошибки редки: функции только вычисляют и возвращают результат, а данные не меняются у тебя за спиной. JavaScript — не чисто функциональный язык, но map, filter, reduce, React и Redux построены именно на этих идеях.

Чистые функции и побочные эффекты

Определение
Чистая функция

1) Для одних и тех же аргументов всегда возвращает один и тот же результат; 2) не имеет побочных эффектов: не меняет внешние переменные и свои аргументы, ничего не выводит, не пишет в файлы и в сеть.

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

JavaScript
let taxRate = 0.18;
const priceWithTaxImpure = (price) => price + price * taxRate;
const priceWithTax = (price, rate) => price + price * rate;

function addItemImpure(cart, item) {
  cart.push(item);
  return cart;
}
const addItem = (cart, item) => [...cart, item];

const cart = ['tea'];
const newCart = addItem(cart, 'bread');
console.log(cart, newCart);

addItemImpure(cart, 'jam');
console.log(cart);

console.log(priceWithTax(100, 0.18), priceWithTaxImpure(100));
taxRate = 0.2;
console.log(priceWithTax(100, 0.18), priceWithTaxImpure(100));
▸ Ожидаемый результат
[ 'tea' ] [ 'tea', 'bread' ]
[ 'tea', 'jam' ]
118 118
118 120
Результат нечистой функции зависит от внешней taxRate и изменился при том же аргументе. А addItemImpure испортила исходную корзину.

Неизменяемость

Неизменяемость означает, что вместо изменения данных ты создаёшь новую версию с изменением. Для массивов и объектов используют spread, но он копирует только один уровень. Во вложенном объекте копируй каждый уровень, который меняешь, а для полной глубокой копии есть structuredClone. Object.freeze(obj) защищает объект от изменений, но тоже действует только на первом уровне.

JavaScript
const user = { name: 'Aysel', address: { city: 'Baku' }, tags: ['student'] };

const moved = { ...user, address: { ...user.address, city: 'Ganja' } };
const tagged = { ...user, tags: [...user.tags, 'volunteer'] };

console.log(user.address.city, moved.address.city);
console.log(user.tags, tagged.tags);

const shallow = { ...user };
shallow.address.city = 'Shaki';
console.log(user.address.city);

const deep = structuredClone(user);
deep.address.city = 'Lankaran';
console.log(user.address.city, deep.address.city);

const scores = [70, 95, 82];
console.log(scores.toSorted((a, b) => b - a), scores);
▸ Ожидаемый результат
Baku Ganja
[ 'student' ] [ 'student', 'volunteer' ]
Shaki
Shaki Lankaran
[ 95, 82, 70 ] [ 70, 95, 82 ]
shallow скопировал только внешний объект, а address остался общим — поэтому и оригинал стал «Shaki». structuredClone копирует все уровни.

Функции высшего порядка, каррирование и композиция

Функция высшего порядка принимает функцию аргументом или возвращает функцию: таковы map, filter, setTimeout. Каррирование превращает f(a, b) в f(a)(b): аргументы можно передавать по частям и делать готовые помощники. Композиция соединяет маленькие функции в конвейер: pipe(f, g, h)(x) — то же самое, что h(g(f(x))).

JavaScript
const add = (a) => (b) => a + b;
const multiply = (a) => (b) => a * b;

const addVat = multiply(1.18);
const addDelivery = add(5);
const round2 = (x) => Math.round(x * 100) / 100;

const pipe = (...fns) => (x) => fns.reduce((value, fn) => fn(value), x);
const compose = (...fns) => (x) => fns.reduceRight((value, fn) => fn(value), x);

const finalPrice = pipe(addVat, addDelivery, round2);

console.log(finalPrice(10));
console.log([10, 25, 99.99].map(finalPrice));
console.log(compose(round2, addDelivery, addVat)(10));
▸ Ожидаемый результат
16.8
[ 16.8, 34.5, 122.99 ]
16.8
pipe применяет функции слева направо, а compose — справа налево, как в математике. round2 скрывает погрешность дробей: 10 * 1.18 на самом деле даёт 11.799999999999999.

Приёмы с reduce

reduce может построить из массива любую структуру: сумму, счётчики, группы, индекс по id, Map. Всегда задавай начальное значение — на пустом массиве reduce без него выбрасывает ошибку. Для группировки в современных браузерах есть и готовый Object.groupBy (ES2024).

JavaScript
const orders = [
  { id: 1, customer: 'Murad', city: 'Baku', total: 40 },
  { id: 2, customer: 'Leyla', city: 'Ganja', total: 25 },
  { id: 3, customer: 'Murad', city: 'Baku', total: 15 },
  { id: 4, customer: 'Elvin', city: 'Baku', total: 60 },
];

const revenue = orders.reduce((sum, order) => sum + order.total, 0);

const countByCity = orders.reduce((acc, { city }) => {
  acc[city] = (acc[city] ?? 0) + 1;
  return acc;
}, {});

const byId = orders.reduce((acc, order) => ({ ...acc, [order.id]: order.customer }), {});

const totalByCustomer = orders.reduce(
  (map, { customer, total }) => map.set(customer, (map.get(customer) ?? 0) + total),
  new Map(),
);

console.log(revenue);
console.log(countByCity);
console.log(byId);
console.log(totalByCustomer);
▸ Ожидаемый результат
140
{ Baku: 3, Ganja: 1 }
{ '1': 'Murad', '2': 'Leyla', '3': 'Murad', '4': 'Elvin' }
Map(3) { 'Murad' => 55, 'Leyla' => 25, 'Elvin' => 60 }
Задание

Напиши чистую функцию applyDiscount(products, percent): она возвращает новый массив со сниженными ценами, а products не меняется. Новая цена: price * (100 - percent) / 100.

Задание · JavaScript
const products = [
  { name: 'Pen', price: 2 },
  { name: 'Notebook', price: 5 },
];

function applyDiscount(products, percent) {
  // return a new array; do not change products
}

console.log(applyDiscount(products, 20));
console.log(products);
▸ Ожидаемый результат
[ { name: 'Pen', price: 1.6 }, { name: 'Notebook', price: 4 } ]
[ { name: 'Pen', price: 2 }, { name: 'Notebook', price: 5 } ]
Задание

Собери slugify из маленьких функций с помощью pipe: обрежь пробелы по краям, переведи текст в нижний регистр, раздели на слова (отбросив пустые части) и соедини слова через -.

Задание · JavaScript
const pipe = (...fns) => (x) => fns.reduce((value, fn) => fn(value), x);

const trim = (text) => text.trim();
const lower = (text) => text.toLowerCase();
// write words(text) and joinWithDash(words)

const slugify = pipe(trim, lower);

console.log(slugify('  Learn JavaScript Today '));
console.log(slugify('Functional   Programming'));
▸ Ожидаемый результат
learn-javascript-today
functional-programming

Главное

  • Чистая функция зависит только от аргументов и не имеет побочных эффектов — её легко тестировать и переиспользовать.
  • Не меняй полученные данные: создавай новые массивы и объекты через spread, map, filter, toSorted.
  • Spread делает поверхностную копию; копируй каждый вложенный уровень, который меняешь, или используй structuredClone.
  • Функции высшего порядка принимают или возвращают функции; каррирование заранее заполняет аргументы, а pipe/compose соединяют маленькие функции.
  • reduce с начальным значением суммирует, считает, группирует и индексирует данные.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какая функция чистая?