Розкриття таємниць куріювання в JavaScript: замикання, часткове застосування, повторне використання
Дізнайтеся, як куріювання перетворює функції JavaScript з кількома аргументами на повторно використовувані ланцюжки з одним аргументом, у чому воно відрізняється від часткового застосування та коли його варто пропустити.
Якщо ви постійно передаєте один і той самий перший аргумент у функцію, за кожен виклик доводиться платити невелику «податок на повторення». Курірування дозволяє зафіксувати частину вхідних даних функції один раз та повторно використовувати отриманий результат. Нижче ви побачите, що таке курірування, як це забезпечують закриття, чим воно відрізняється від часткового застосування та коли воно справді покращує код.
Що означає курірування
Курірування перетворює функцію, яка приймає кілька аргументів, на послідовність функцій, кожна з яких приймає рівно один аргумент. Звичайний виклик передає всі дані одночасно:
f(a, b, c)
У курірованій формі з кожного виклику передається одне значення, а кожен виклик повертає наступну функцію в ланцюзі:
f(a)(b)(c)
Кожна проміжна функція пам’ятає значення з попередніх викликів. Ця пам’ять забезпечується закриттями, про які йтиметься нижче.
Перетворення звичайної функції
Почнемо зі звичайної функції додавання двох аргументів:
function sum(a, b) {
return a + b;
}
sum(2, 3); // 5
Невеликий допоміжний функціонал може обгортати будь-яку функцію з двома аргументами: зовнішня функція бере перше значення, повертає функцію, яка приймає друге значення, а лише потім викликає оригінальну функцію з обома аргументами:
function curry(fn) {
return function (a) {
return function (b) {
return fn(a, b);
};
};
}
const curriedSum = curry(sum);
curriedSum(2)(3); // 5
Що варто зазначити:
curryє функцією вищого порядку: як її вхід, так і вихід — це функції.- Кожна функція, яку вона повертає, очікує лише одне значення.
- Внутрішня функція все ще може отримувати доступ до
aпісля того, як зовнішній виклик повернувся, оскільки вона зберігає доступ до нього.
Цей допоміжний функціонал обробляє лише рівно два параметри. Універсальні версії перевіряють fn.length та продовжують збирати аргументи, поки не накопичиться достатня кількість, але ідея залишається такою ж.
Практичний приклад: спеціалізовані логери
Арифметичні операції приховують сутність рішення, тож розгляньмо логер, який приймає рівень серйозності та повідомлення:
function log(level, message) {
console.log(`[${level}] ${message}`);
}
Місця виклику швидко починають повторювати один і той самий рівень:
log("ERROR", "Something broke");
log("ERROR", "Invalid token");
log("ERROR", "API failed");
Курірований логгер дозволяє створити заздалегідь налаштовані логгери лише один раз:
function log(level) {
return function (message) {
console.log(`[${level}] ${message}`);
};
}
const errorLog = log("ERROR");
const infoLog = log("INFO");
errorLog("Something broke");
errorLog("Invalid token");
infoLog("User logged in");
Рівень фіксується під час створення errorLog та infoLog. Місця виклику стають коротшими, намір читається легко, а спеціалізовані функції можна експортувати чи вставляти в інші місця. Саме поєднання повторного використання та виразності є основною причиною використання курірування.
Часткове застосування
Часткове застосування означає попереднє заповнення деяких аргументів існуючої функції для отримання нової функції без зміни її форми. JavaScript підтримує це нативно за допомогою bind:
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
double(5); // 10
Перший аргумент функції bind встановлює значення this, яке функція multiply ігнорує, тому результатом є null. Функція multiply все одно приймає два аргументи; double — це просто варіант, у якому перший аргумент вже заданий.
Куріювання проти часткового застосування
Обидва підходи мають спільну мету, але різні механізми:
- Куріювання перетворює функцію на ланцюг функцій з одним аргументом.
- Часткове застосування заповнює деякі аргументи вже зараз, залишаючи структуру функції незмінною.
Кожен крок у ланцюзі, створеному шляхом куріювання, фактично є частковим застосуванням з одним аргументом, проте часткове застосування ніколи не вимагає куріювання.
Безкінечне куріювання
Ігровий варіант продовжує приймати значення, поки ви не завершите ланцюг порожнім викликом:
let infiniteSum = function (a) {
return function (b) {
if (b !== undefined) return infiniteSum(a + b);
return a;
};
};
infiniteSum(1)(2)(3)(4)(); // 10
Як це відбувається:
- Кожен виклик із значенням додає його до поточної суми та повертає нову функцію, яка містить цю суму.
- Останній виклик
()залишаєbу значенніundefined, тому повертається сума. - Замикання передають суму з одного кроку на інший.
Явна перевірка b !== undefined має велике значення. Тест на істинність, наприклад if (b), вважатиме 0 кінцевою точкою, тому infiniteSum(1)(0)(2)() зупиниться на нулі замість того, щоб повернути 3. Ця схема є чудовим вправою з замикань та поширеним запитанням на співбесіді, але легко забуваний останній виклик робить її поганою альтернативою для продакшн-API.
Замикання забезпечують правильну роботу
Ось найменший курірований додавач:
function add(a) {
return function (b) {
return a + b;
};
}
add(3)(4); // 7
add(3) повертає внутрішню функцію та завершується, проте ця внутрішня функція все ще може читати значення a. Це і є замиканням: функція зберігає доступ до змінних з того області видимості, де вона була визначена, навіть після того, як зовнішня функція повернулася. Оскільки зафіксоване значення зберігається, можна зберігати та повторно використовувати частково застосовану функцію:
const add3 = add(3);
add3(4); // 7
add3(10); // 13
Щоб дізнатися більше про те, як вирішуються ці зафіксовані змінні, перегляньте як ланцюг областей видимості вирішує змінні.
Коли його використовувати, а коли пропускати
Курірування є корисним у таких випадках:
- однакові аргументи з’являються у багатьох місцях виклику
- воно робить код зрозумілішим, а не більш заплутаним
- ви хочете створювати маленькі, комбіновані та повторно використовувані функції
Пропустіть це, коли:
- проста одноразова функція не отримує жодної переваги від розділення
- ваша команда не знайома з цим підходом
- ланцюгові виклики погіршують читабельність
Підсумок
Куріювання полягає у тому, щоб впорядкувати вхідні параметри функції за моментом їхнього отримання: конфігурація, така як рівень логування чи базова URL, встановлюється заздалегідь, а дані для кожного виклику надходять пізніше. Клозури забезпечують зберігання цих даних, bind дозволяє використовувати функцію частково, коли не потрібен повний ланцюг викликів, а ключове питання завжди полягає у тому, чи стає код виклику легшим для читання та повторного використання.
Пов’язана література
- Розуміння замикань у JavaScript та циклу подій — Дізнайтеся, як замикання зберігають змінні зовнішнього контексту та як цикл подій організовує стек викликів, мікрозавдання та макрозавдання за допомогою практичних прикладів коду.
- Як насправді розв’язується проблема змінних у ланцюзі областей видимості JavaScript — Пояснюється механізм лексичного ланцюга областей видимості, який використовується для пошуку змінних, чому використання ключового слова var спричиняє проблеми в циклах та як замикання є природним наслідком цього механізму.