- Отличать чистые функции от нечистых, избегать побочных эффектов и изменения данных
- Обновлять массивы и объекты без изменения исходных: spread,
map,toSorted,structuredClone - Строить маленькие переиспользуемые функции с помощью функций высшего порядка, каррирования и композиции (
pipe/compose) - Использовать
reduceдля группировки, подсчёта и индексации данных
Приходит жалоба: «Сумма в корзине иногда неверная». Час поисков — и находишь функцию, которая молча изменила переданный ей массив. Функциональное программирование (ФП) — стиль, в котором такие ошибки редки: функции только вычисляют и возвращают результат, а данные не меняются у тебя за спиной. JavaScript — не чисто функциональный язык, но map, filter, reduce, React и Redux построены именно на этих идеях.
Чистые функции и побочные эффекты
1) Для одних и тех же аргументов всегда возвращает один и тот же результат; 2) не имеет побочных эффектов: не меняет внешние переменные и свои аргументы, ничего не выводит, не пишет в файлы и в сеть.
Программ без побочных эффектов не бывает — результат рано или поздно нужно показать или сохранить. ФП выносит их на края программы: ядро состоит из чистых функций, которые легко тестировать, а ввод-вывод происходит в нескольких понятных местах.
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) защищает объект от изменений, но тоже действует только на первом уровне.
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))).
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).
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.
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: обрежь пробелы по краям, переведи текст в нижний регистр, раздели на слова (отбросив пустые части) и соедини слова через -.
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.