Раскрытие тайн кюрирования в 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 приводит к ошибкам в циклах, и как из этого естественным образом возникают замыкания.