Каррирование функций JavaScript

внешний интерфейс Шаблоны проектирования JavaScript API

Что такое карри?

официальное заявление

В информатике,карри(Английский:Currying), также переводится как卡瑞化или加里化, — это метод преобразования функции, которая принимает несколько параметров, в функцию, которая принимает один параметр (первый параметр исходной функции) и возвращает новую функцию, которая принимает остальные параметры и возвращает результат. Эта технология克里斯托弗·斯特雷奇как логик哈斯凯尔·加里назван, хотя иMoses Schönfinkelа также戈特洛布·弗雷格发明的.

Интуитивно выдвигая претензииЕсли вы зафиксируете некоторые параметры, вы получите функцию, которая принимает остальные параметры..
В теоретической информатике каррирование обеспечивает простые теоретические модели, такие как:lambdaВ исчислении способ изучения функций с несколькими аргументами.
Двойная функция каррированияUncurrying, способ реализации функций с несколькими аргументами с использованием анонимных функций с одним аргументом.

Легко понять

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

Если нам нужно реализовать функцию, которая суммирует три числа:

function add(x, y, z) {
  return x + y + z;
}
console.log(add(1, 2, 3)); // 6
var add = function(x) {
  return function(y) {
    return function(z) {
      return x + y + z;
    }
  }
}

var addOne = add(1);
var addOneAndTwo = addOne(2);
var addOneAndTwoAndThree = addOneAndTwo(3);

console.log(addOneAndTwoAndThree);

Здесь мы определяемadd函数,它接受一个参数并返回一个新的函数。 передачаaddПосле этого возвращаемая функция запоминается через замыканиеaddпервый параметр . Вызывать все сразу немного утомительно, но, к счастью, мы можем использовать специальныйcurryвспомогательная функция (helper function) упрощает определение и вызов таких функций.

использоватьES6Функция стрелки, мы можем поставить верхнююaddРеализовано так:

const add = x => y => z => x + y + z;

Кажется, намного понятнее использовать стрелочные функции.

Частичная функция?

Взгляните на эту функцию:

function ajax(url, data, callback) {
  // ..
}

Есть такой сценарий: нам нужно инициировать несколько разных интерфейсовHTTPЕсть два способа отправить запрос:

  • вызовajax()При вызове функции передать глобальныйURLпостоянный.
  • Создать уже предустановленнуюURLСсылка на функцию для аргумента.

Ниже мы создаем новую функцию, которая по-прежнему срабатывает внутри.ajax()request, и, ожидая получения двух других аргументов, вручнуюajax()Установите первый аргумент на все, что вам нужноAPIадрес.

Для первого подхода мы можем сгенерировать следующие методы вызова:

function ajaxTest1(data, callback) {
  ajax('http://www.test.com/test1', data, callback);
}

function ajaxTest2(data, callback) {
  ajax('http://www.test.com/test2', data, callback);
}

Для этих двух похожих функций мы также можем извлечь следующие шаблоны:

function beginTest(callback) {
  ajaxTest1({
    data: GLOBAL_TEST_1,
  }, callback);
}

Я уверен, что вы видели этот шаблон: мы вызываем функцию на сцене (function call-site), применяя аргументы (apply) по формальному параметру. Как видите, мы начали с применения только некоторых аргументов, в частности, применяя аргументы кURLФормальные параметры - остальные фактические параметры применяются позже.

Вышеупомянутая концепциячастичная функцияОпределение частичной функции — это процесс сокращения количества параметров функции; количество параметров здесь относится к количеству формальных параметров, которые вы хотите передать. мы проходимajaxTest1()поставить оригинальную функциюajax()Количество параметров из3Сокращение до2индивидуальный.

Мы определяемpartial()функция:

function partial(fn, ...presetArgs) {
  return function partiallyApplied(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  }
}

partial()функция полученияfnпараметры, чтобы указать, что мы неравнодушны к фактическим параметрам (partially apply)Функция. тогда,fnПосле формальных параметровpresetArgsМассив собирает фактические параметры, переданные позже, и сохраняет их для последующего использования.

мы создаем иreturnсоздал новую внутреннюю функцию (для ясности мы назвали ееpartiallyApplied(..)), в этой функцииlaterArgsМассив собирает все аргументы.

Используя стрелочные функции, это более лаконично:

var partial =
  (fn, ...presetArgs) =>
    (...laterArgs) =>
      fn(...presetArgs, ...laterArgs);

Используя этот шаблон частичных функций, мы рефакторим предыдущий код:

function ajax(url, data, callback) {
  // ..
}

var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');
var ajaxTest2 = partial(ajax, 'http://www.test.com/test1');

подумай еще разbeginTest()функция, мы используемpartial()Как его рефакторить?

function ajax(url, data, callback) {
  // ..
}

// 版本1
var beginTest = partial(ajax, 'http://www.test.com/test1', {
  data: GLOBAL_TEST_1,
});

// 版本2
var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');
var beginTest = partial(ajaxTest1, {
  data: GLOBAL_TEST_1,
});

проходить по одному

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

The process of converting a function that takes multiple arguments into a function that takes them one at a time.

Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.

Предположим, мы создали каррированную версиюajax()функцияcurriedAjax():

curriedAjax('http://www.test.com/test1')
  ({
    data: GLOBAL_TEST_1,
  })
  (function callback(data) {
    // dosomething
  });

Давайте разберем три вызова по отдельности, что может помочь нам понять весь процесс:

var ajaxTest1 = curriedAjax('http://www.test.com/test1');

var beginTest = ajaxTest1({
  data: GLOBAL_TEST_1,
});

var ajaxCallback = beginTest(function callback(data) {
  // dosomething
});

Реализовать каррирование

Итак, как нам реализовать функцию автоматического каррирования?

var currying = function(fn) {
  var args = [];

  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args); // 没传参数时,调用这个函数
    } else {
      [].push.apply(args, arguments); // 传入了参数,把参数保存下来
      return arguments.callee; // 返回这个函数的引用
    }
  }
}

позвонить вышеcurrying()функция:

var cost = (function() {
  var money = 0;
  return function() {
    for (var i = 0; i < arguments.length; i++) {
      money += arguments[i];
    }
    return money;
  }
})();

var cost = currying(cost);

cost(100); // 传入了参数,不真正求值
cost(200); // 传入了参数,不真正求值
cost(300); // 传入了参数,不真正求值

console.log(cost()); // 求值并且输出600

Вышеупомянутая функция - моя предыдущаяЗамыкания и функции высшего порядка в шаблонах проектирования JavaScript и примечания к чтению по практике разработкинаписаноcurryingверсии, и теперь, после тщательного рассмотрения, я обнаружил, что некоторые проблемы все же есть.

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

Поэтому измените приведенную выше функцию каррирования следующим образом:

var currying = function(fn) {
  var args = Array.prototype.slice.call(arguments, 1);

  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args); // 没传参数时,调用这个函数
    } else {
      [].push.apply(args, arguments); // 传入了参数,把参数保存下来
      return arguments.callee; // 返回这个函数的引用
    }
  }
}

Пример использования:

var cost = (function() {
  var money = 0;
  return function() {
    for (var i = 0; i < arguments.length; i++) {
      money += arguments[i];
    }
    return money;
  }
})();

var cost = currying(cost, 100);
cost(200); // 传入了参数,不真正求值
cost(300); // 传入了参数,不真正求值

console.log(cost()); // 求值并且输出600

Вам может показаться, что вам нужно каждый раз вызывать функцию без параметров в конце.cost()функция громоздкая, и вcost()функция для использованияargumentsПараметры не те, что вы ожидали. Мы знаем, что функции имеютlengthатрибут, указывающий количество аргументов, которые функция ожидает принять. Таким образом, мы можем в полной мере использовать эту возможность предварительной передачи параметров.

заимствовано изmqyqingfeng:

function sub_curry(fn) {
  var args = [].slice.call(arguments, 1);
  return function() {
    return fn.apply(this, args.concat([].slice.call(arguments)));
  };
}

function curry(fn, length) {

  length = length || fn.length;

  var slice = Array.prototype.slice;

  return function() {
    if (arguments.length < length) {
      var combined = [fn].concat(slice.call(arguments));
      return curry(sub_curry.apply(this, combined), length - arguments.length);
    } else {
      return fn.apply(this, arguments);
    }
  };
}

В приведенной выше функции, в возвращаемой функции каррирования, мы помещаемarguments.lengthа такжеfn.lengthдля сравнения один разarguments.lengthДостигfn.lengthномер, мы позвонимfn(return fn.apply(this, arguments);)

проверять:

var fn = curry(function(a, b, c) {
  return [a, b, c];
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

Реализация метода привязки

Используя каррирование, легко заимствоватьcall()илиapply()выполнитьbind()методpolyfill.

Function.prototype.bind = Function.prototype.bind || function(context) {
  var me = this;
  var args = Array.prototype.slice.call(arguments, 1);
  return function() {
    var innerArgs = Array.prototype.slice.call(arguments);
    var finalArgs = args.concat(innerArgs);
    return me.apply(contenxt, finalArgs);
  }
}

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

Function.prototype.bind() by MDNВ нем говорится следующее:

Функции привязки применяют новый оператор new для создания нового экземпляра, созданного целевой функцией. Когда связанная функция используется для создания значения, изначально предоставленное this игнорируется. Однако те параметры, которые были предоставлены изначально, по-прежнему добавляются перед вызовом конструктора.

ЭтоРазработка полнофункциональных веб-приложений JavaScript на основе MVCизbind()Реализация метода:

Function.prototype.bind = function(oThis) {
  if (typeof this !== "function") {
    throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  }

  var aArgs = Array.prototype.slice.call(arguments, 1),
    fToBind = this,
    fNOP = function() {},
    fBound = function() {
      return fToBind.apply(
        this instanceof fNOP && oThis ? this : oThis || window,
        aArgs.concat(Array.prototype.slice.call(arguments))
      );
    };

  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();

  return fBound;
};

Некаррирование

Вы можете столкнуться с такой ситуацией: вы получаете каррированную функцию, но вам нужна версия до ее каррирования, которая, по сути, пытается преобразовать что-то вродеf(1)(2)(3)Функция возвращается к чему-то вродеg(1,2,3)Функция.

Ниже простоuncurryingреализация:

function uncurrying(fn) {
  return function(...args) {
    var ret = fn;

    for (let i = 0; i < args.length; i++) {
      ret = ret(args[i]); // 反复调用currying版本的函数
    }

    return ret; // 返回结果
  };
}

Обратите внимание: не предполагайте, что функция после отмены каррирования такая же, как и функция до каррирования, они просто ведут себя одинаково!

var currying = function(fn) {
  var args = Array.prototype.slice.call(arguments, 1);

  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args); // 没传参数时,调用这个函数
    } else {
      [].push.apply(args, arguments); // 传入了参数,把参数保存下来
      return arguments.callee; // 返回这个函数的引用
    }
  }
}

function uncurrying(fn) {
  return function(...args) {
    var ret = fn;

    for (let i = 0; i < args.length; i++) {
      ret = ret(args[i]); // 反复调用currying版本的函数
    }

    return ret; // 返回结果
  };
}

var cost = (function() {
  var money = 0;
  return function() {
    for (var i = 0; i < arguments.length; i++) {
      money += arguments[i];
    }
    return money;
  }
})();

var curryingCost = currying(cost);
var uncurryingCost = uncurrying(curryingCost);
console.log(uncurryingCost(100, 200, 300)()); // 600

Какая польза от каррирования или частичных функций?

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

Еще одна вещь, которая лучше всего отражает применение каррирования, заключается в том, что когда функции имеют только один параметр, мы можем относительно легко комбинировать их (单一职责原则(Single responsibility principle)). Таким образом, если функция принимает три аргумента, она превращается в функцию, которая принимает три вызова, по одному для каждого. Эта форма единичных функций облегчает нам задачу при составлении функций.

Подвесками, в основном для следующих трех распространенных применений:

  • Расчет задержки
  • Повторное использование параметра
  • Динамически генерируемые функции