[Дополнительно 6-2] Углубленное каррирование приложений функций высшего порядка

внешний интерфейс JavaScript

Обновление: Спасибо за вашу поддержку. Недавно я бросил сводку данных для всех, чтобы прочитать систему. В будущем будет больше контента и больше оптимизации.Нажмите здесь, чтобы просмотреть

------ Далее идет текст ------

введение

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

Если у вас есть какие-либо идеи или мнения, вы можете оставить сообщение в области комментариев.Следующее изображение представляет собой интеллект-карту этой статьи.Пожалуйста, смотрите мою интеллект-карту в высоком разрешении и другие статьи.Github.

【进阶 6-2 期】深入高阶函数应用之柯里化

карри

определение

Каррирование функции также называется частичной оценкой.Википедия определяет каррирование как:

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

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

// 木易杨
const add = (...args) => args.reduce((a, b) => a + b);

// 传入多个参数,执行 add 函数
add(1, 2) // 3

// 假设我们实现了一个 currying 函数,支持一次传入一个参数
let sum = currying(add);
// 封装第一个参数,方便重用
let addCurryOne = sum(1);
addCurryOne(2) // 3
addCurryOne(3) // 4

практическое применение

1. Расчет задержки

Давайте посмотрим на следующий пример частичного суммирования, который прекрасно иллюстрирует случай отложенных вычислений.

// 木易杨
const add = (...args) => args.reduce((a, b) => a + b);

// 简化写法
function currying(func) {
    const args = [];
    return function result(...rest) {
        if (rest.length === 0) {
          return func(...args);
        } else {
          args.push(...rest);
        	return result;
        }
    }
}

const sum = currying(add);

sum(1,2)(3); // 未真正求值
sum(4); 		 // 未真正求值
sum(); 			 // 输出 10

Приведенный выше код прост для понимания, то есть «используйте замыкание для сохранения входящих параметров и начинайте выполнение функции, когда количество входящих параметров достаточно для выполнения функции». Приведенная выше функция каррирования представляет собой упрощенный способ записи, который определяет, равна ли длина входящего параметра 0, если она равна 0, выполнить функцию, иначе собрать параметры.

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

// 木易杨
let obj = {
  name: 'muyiy'
}
const fun = function () {
  console.log(this.name);
}.bind(obj);

fun(); // muyiy

здесьbindОн используется для изменения контекста, когда функция выполняется, но сама функция не выполняется, поэтому по сути это отложенное вычисление, похожее наcall / applyНепосредственное исполнение имеет значение.

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

Следующая схема реализации представляет собой упрощенную версию реализации, полную версию процесса реализации и интерпретации кода можно найти в статье, которую я написал ранее.[Расширенная фаза 3-4] Углубленный анализ принципа связывания, сценариев использования и реализации моделирования.

// 木易杨
// 简化实现,完整版实现中的第 2 步
Function.prototype.bind = function (context) {
    var self = this;
    // 第 1 个参数是指定的 this,截取保存第 1 个之后的参数
		// arr.slice(begin); 即 [begin, end]
    var args = Array.prototype.slice.call(arguments, 1); 

    return function () {
        // 此时的 arguments 是指 bind 返回的函数调用时接收的参数
        // 即 return function 的参数,和上面那个不同
      	// 类数组转成数组
        var bindArgs = Array.prototype.slice.call(arguments);
      	// 执行函数
        return self.apply( context, args.concat(bindArgs) );
    }
}

2, динамически созданная функция

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

Давайте рассмотрим следующий пример. При добавлении событий в модель DOM она должна быть совместима с современными браузерами и браузерами IE (IE

// 简化写法
function addEvent (type, el, fn, capture = false) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, capture);
    }
    else if(window.attachEvent){
        el.attachEvent('on' + type, fn);
    }
}

Но есть проблема с таким способом записи, то есть каждый раз, когда добавляется событие, оно будет вызываться для вынесения суждения, так есть ли способ судить о нем только один раз? выражения функции вызова (IIFE).

const addEvent = (function(){
    if (window.addEventListener) {
        return function (type, el, fn, capture) {
            el.addEventListener(type, fn, capture);
        }
    }
    else if(window.attachEvent){
        return function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
})();

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

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

function addEvent (type, el, fn, capture = false) {
  	// 重写函数
    if (window.addEventListener) {
        addEvent = function (type, el, fn, capture) {
            el.addEventListener(type, fn, capture);
        }
    }
    else if(window.attachEvent){
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
  	// 执行函数,有循环爆栈风险
  	addEvent(type, el, fn, capture); 
}

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

3. Повторное использование параметров

мы знаем, как позвонитьtoString()Можно получить тип каждого объекта, ноtoString()Существуют разные реализации, поэтому нужно пройтиObject.prototype.toString()получитьObjectреализации вышеизложенного, при этомcall() / apply()и передайте проверяемый объект в качестве первого параметра, как в следующем примере.

function isArray(obj) { 
    return Object.prototype.toString.call(obj) === '[object Array]';
}

function isNumber(obj) {
    return Object.prototype.toString.call(obj) === '[object Number]';
}

function isString(obj) {
    return Object.prototype.toString.call(obj) === '[object String]';
}

// Test
isArray([1, 2, 3]); // true
isNumber(123); // true
isString('123'); // true

Но есть проблема с приведенной выше схемой, то есть для каждого типа нужно определить метод, здесь мы можем использовать bind для расширения,преимуществоможно использовать сразу после модификацииtoStr.

const toStr = Function.prototype.call.bind(Object.prototype.toString);

// 改造前
[1, 2, 3].toString(); // "1,2,3"
'123'.toString(); // "123"
123.toString(); // SyntaxError: Invalid or unexpected token
Object(123).toString(); // "123"

// 改造后
toStr([1, 2, 3]); 	// "[object Array]"
toStr('123'); 		// "[object String]"
toStr(123); 		// "[object Number]"
toStr(Object(123)); // "[object Number]"

В приведенном выше примере сначала используетсяFunction.prototype.callфункция определяетthisзначение, то.bindВозвращает новую функцию, которая всегдаObject.prototype.toStringУстановить как входящий параметр, который фактически эквивалентенObject.prototype.toString.call().

Реализовать функцию каррирования

Мы можем понять так называемую функцию каррирования, которая должна инкапсулировать «серию шагов обработки», собирать параметры через замыкание и, наконец, передавать параметры, которые необходимо обработать. Так как же реализовать функцию каррирования?

Принцип реализации: «использовать замыкание для сохранения входящих параметров и начинать выполнение функции, когда количество входящих параметров достаточно для выполнения функции». В приведенном выше разделе ленивых вычислений реализована упрощенная версия функции каррирования.

Теперь давайте реализуем более надежную функцию каррирования.

// 木易杨
function currying(fn, length) {
  length = length || fn.length; 	// 注释 1
  return function (...args) {			// 注释 2
    return args.length >= length	// 注释 3
    	? fn.apply(this, args)			// 注释 4
      : currying(fn.bind(this, ...args), length - args.length) // 注释 5
  }
}

// Test
const fn = currying(function(a, b, c) {
    console.log([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"]
  • Примечание 1. Первый вызов получает длину параметра fn функции, а последующие вызовы получают длину остальных параметров fn.

  • Примечание 2: после переноса каррирования возвращается новая функция, а параметры...args

  • Примечание 3. Если длина параметров, полученных новой функцией, больше или равна длине, которую должны получить остальные параметры fn.

  • Примечание 4. Выполните требования, выполните функцию fn и передайте параметры новой функции.

  • Примечание 5: Не выполнено, рекурсивная каррирующая функция, новая fnbindНовая функция, которая возвращает (bindграница...argsпараметр, не реализован), новая длина равна длине оставшихся параметров fn

В приведенном выше примере используется сочетание синтаксиса ES5 и ES6, поэтому я не хочу использоватьcall/apply/bindЭти методы, естественно, возможны.

// 参考自 segmentfault 的@大笑平 
const currying = fn =>
    judge = (...args) =>
        args.length >= fn.length
            ? fn(...args)
            : (...arg) => judge(...args, ...arg)

// Test
const fn = currying(function(a, b, c) {
    console.log([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"]

Если вы все еще не можете понять это, вам будет легче понять это после прочтения следующего примера.Чтобы реализовать метод add, вам необходимо соответствовать следующим ожиданиям.

add(1, 2, 3) // 6
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2, 3) // 6

Мы можем видеть, что результатом вычисления является сумма всех параметров, если мы вызовем его дваждыadd(1)(2), вы можете написать следующий код.

function add(a) {
  return function(b) {
  	return a + b;
  }
}

add(1)(2) // 3

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

Если его вызвать три разаadd(1)(2)(3), вы можете написать следующий код.

function add(a) {
  return function(b) {
    return function (c) {
    	return a + b + c;
    }
  }
}
console.log(add(1)(2)(3)); // 6

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

На этот раз давайте посмотрим на функцию реализации каррирования. По сути, она должна судить, достаточно ли текущей длины параметра. Если параметров достаточно, она будет выполнена немедленно. Если недостаточно, будет создана новая функция. эта новая функция не будет выполнена, иbindИли закрытие сохраняет параметры, переданные ранее.

// 注释同上
function currying(fn, length) {
  length = length || fn.length; 	
  return function (...args) {			
    return args.length >= length	
    	? fn.apply(this, args)			
      : currying(fn.bind(this, ...args), length - args.length) 
  }
}

Расширение: длина параметра функции

При реализации функции каррирования с использованиемfn.lengthдля представления количества параметров функции, затемfn.lengthПредставлять количество всех параметров функции? Не совсем.

Свойство length функции получает количество формальных параметров, но количество формальных параметров не включает количество оставшихся параметров, а включает только количество параметров до того, как первый из них получит значение по умолчанию, см. следующий пример.

((a, b, c) => {}).length; 
// 3

((a, b, c = 3) => {}).length; 
// 2 

((a, b = 2, c) => {}).length; 
// 1 

((a = 1, b, c) => {}).length; 
// 0 

((...args) => {}).length; 
// 0

const fn = (...args) => {
  console.log(args.length);
} 
fn(1, 2, 3)
// 3

Таким образом, в сценариях Collie не рекомендуется использовать параметры функции ES6.

const fn = currying((a = 1, b, c) => {
  console.log([a, b, c]); 
}); 

fn();
// [1, undefined, undefined]

fn()(2)(3); 
// Uncaught TypeError: fn(...) is not a function

Мы ожидаем, что функция fn выдаст[1, 2, 3], но при фактическом вызове каррированной функции((a = 1, b, c) => {}).length === 0, так что звонитеfn()был выполнен и выведен[1, undefined, undefined], вместо идеальной функции замыкания возврата, поэтому последующие вызовыfn()(2)(3)сообщит об ошибке.

резюме

Мы узнали, что такое функция каррирования по определению, и представили три сценария практического применения: отложенное вычисление, создание динамической функции, повторное использование параметров, а затем реализовали мощную обобщенную функцию каррирования, но она больше похожа на каррирование. и partial function (частичное применение), а длина функции вводится в конце, что приятно удивило.

  • Определение: Каррирование — это метод преобразования функции, которая принимает несколько аргументов, в серию функций, которые принимают один аргумент и возвращают новую функцию, которая принимает оставшиеся аргументы и возвращает результат.
  • практическое применение
    • Ленивые вычисления: частичная сумма, функция связывания
    • Динамически создавать функции: добавлять прослушиватель addEvent, ленивую функцию
    • Повторное использование параметра:Function.prototype.call.bind(Object.prototype.toString)
  • Реализуйте функцию каррирования: используйте замыкание для сохранения входящих параметров и начните выполнение функции, когда количество входящих параметров будет достаточно для выполнения функции.
  • Длина параметра функции: получено количество формальных параметров, но количество формальных параметров не включает количество оставшихся параметров, а включает только количество параметров до того, как первый из них получит значение по умолчанию.

использованная литература

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

Ленивые функции в JavaScript

Каковы преимущества каррирования в инженерии?

Челнок статей