Главная / Статьи / Раскрытие тайн кюрирования в JavaScript: замыкания, частичное применение, повторное использование

Раскрытие тайн кюрирования в JavaScript: замыкания, частичное применение, повторное использование

Узнайте, как функция керрирования превращает функции JavaScript с несколькими аргументами в повторно используемые цепочки с одним аргументом, в чём их различие с частичным применением, и когда стоит обойтись без неё.

1051 слов

Если вы постоянно передаёте в функцию один и тот же первый аргумент, за каждый вызов взимается небольшая плата за повторение. Куррирование позволяет фиксировать часть входных данных функции один раз и повторно использовать полученный результат. Ниже вы узнаете, что такое куррирование, каким образом оно обеспечивается закрытыми функциями, в чём его отличие от частичного применения и когда оно действительно улучшает код.

Что означает куррирование

Куррирование преобразует функцию, принимающую несколько аргументов, в последовательность функций, каждая из которых принимает ровно один аргумент. Обычный вызов передаёт все данные сразу:

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

Чтобы узнать больше о том, как решаются эти зафиксированные переменные, ознакомьтесь с том, как цепочка областей видимости решает переменные.

Когда использовать это и когда пропустить

Куррирование оправдано в следующих случаях:

  • одни и те же начальные аргументы появляются во многих местах вызова
  • это делает код более понятным, а не более запутанным
  • вам нужны небольшие, комбинируемые и повторно используемые функции
  • Вы создаете утилиты вроде логгеров, валидаторов, форматтеров или помощников API, где значение конфигурации известно заранее
  • Пропустите этот подход, если:

    • простая одноразовая функция не выиграет от разбиения на части
    • ваша команда не знакома с этим подходом
    • цепочка вызовов ухудшает читаемость кода

    Заключение

    Кюрирование на самом деле сводится к упорядочиванию входных параметров функции в зависимости от момента их получения: конфигурация, такая как уровень логирования или базовый URL, задаётся заранее, а данные для каждого вызова поступают позже. Закрытия помогают запоминать информацию, метод bind позволяет выполнять частичное применение функции, когда не требуется полная цепочка вызовов, а ключевой вопрос всегда заключается в том, становится ли код проще для чтения и повторного использования.

    Связанные материалы

  • Миграция на htmx 4: Fetch, явное наследование и что может сломаться — Ознакомьтесь с изменениями в архитектуре htmx 4, начиная с ядра, основанного на Fetch, и явного наследования атрибутов, до механизмов обмена ошибками, преобразования элементов и поддержки истории изменений, а также спланируйте безопасную миграцию.