«Front-end Advanced» полностью понимает каррирование функций.

JavaScript
«Front-end Advanced» полностью понимает каррирование функций.

Чем больше вы знаете, тем больше вы не знаете
点赞Посмотри еще раз, аромат остался в руке, и слава

предисловие

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

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

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

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

// 数学和计算科学中的柯里化:

//一个接收三个参数的普通函数
function sum(a,b,c) {
    console.log(a+b+c)
}

//用于将普通函数转化为柯里化版本的工具函数
function curry(fn) {
  //...内部实现省略,返回一个新函数
}

//获取一个柯里化后的函数
let _sum = curry(sum);

//返回一个接收第二个参数的函数
let A = _sum(1);
//返回一个接收第三个参数的函数
let B = A(2);
//接收到最后一个参数,将之前所有的参数应用到原函数中,并运行
B(3)    // print : 6

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

Каррированные функции в математике и информатике, которым можно передать только один параметр за раз;

Каррированная функция в нашем практическом приложении Javascript может передавать один или несколько параметров.

Взгляните на этот пример:

//普通函数
function fn(a,b,c,d,e) {
  console.log(a,b,c,d,e)
}
//生成的柯里化函数
let _fn = curry(fn);

_fn(1,2,3,4,5);     // print: 1,2,3,4,5
_fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
_fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

Для каррированной функции _fn, когда количество полученных параметров совпадает с количеством формальных параметров исходной функции, выполняется исходная функция; Когда количество полученных параметров меньше количества формальных параметров исходной функции, функция возвращается для получения оставшихся параметров, и исходная функция выполняется до тех пор, пока количество полученных параметров не станет таким же, как количество формальных параметров. .

Когда мы узнаем, что такое каррирование, давайте посмотрим, в чем его польза?

Использование карри

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

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

function checkByRegExp(regExp,string) {
    return regExp.test(string);  
}

checkByRegExp(/^1\d{10}$/, '18642838455'); // 校验电话号码
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校验邮箱

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

Мы могли бы сделать это:

checkByRegExp(/^1\d{10}$/, '18642838455'); // 校验电话号码
checkByRegExp(/^1\d{10}$/, '13109840560'); // 校验电话号码
checkByRegExp(/^1\d{10}$/, '13204061212'); // 校验电话号码

checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校验邮箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@qq.com'); // 校验邮箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@gmail.com'); // 校验邮箱

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

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

//进行柯里化
let _check = curry(checkByRegExp);
//生成工具函数,验证电话号码
let checkCellPhone = _check(/^1\d{10}$/);
//生成工具函数,验证邮箱
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);

checkCellPhone('18642838455'); // 校验电话号码
checkCellPhone('13109840560'); // 校验电话号码
checkCellPhone('13204061212'); // 校验电话号码

checkEmail('test@163.com'); // 校验邮箱
checkEmail('test@qq.com'); // 校验邮箱
checkEmail('test@gmail.com'); // 校验邮箱

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

После каррирования мы генерируем две функции checkCellPhone и checkEmail, Функция checkCellPhone может только проверить, является ли входящая строка номером телефона, Функция checkEmail может только проверить, является ли входящая строка электронным письмом, По сравнению с исходной функцией checkByRegExp они функционально менее универсальны, но их применимость улучшена. Это использование каррирования можно понимать как:Повторное использование параметра

Давайте посмотрим на другой пример

Предположим, у нас есть этот фрагмент данных:

let list = [
    {
        name:'lucy'
    },
    {
        name:'jack'
    }
]

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

let names = list.map(function(item) {
  return item.name;
})

Так как же нам это сделать с помощью каррированного мышления?

let prop = curry(function(key,obj) {
    return obj[key];
})
let names = list.map(prop('name'))

Увидев это, у вас могут возникнуть сомнения: зачем реализовывать функцию prop в таком простом примере только для того, чтобы получить значение атрибута name, это слишком хлопотно.

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

Наш фактический код можно понять как только одну строку let names = list.map(prop('name'))

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

Как инкапсулировать каррированные служебные функции

Далее давайте подумаем, как реализовать функцию карри.

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

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

У нас есть две идеи:

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

Мы объединяем эти две точки следующим образом, чтобы реализовать простую функцию карри:

/**
 * 将函数柯里化
 * @param fn    待柯里化的原函数
 * @param len   所需的参数个数,默认为原函数的形参个数
 */
function curry(fn,len = fn.length) {
    return _curry.call(this,fn,len)
}

/**
 * 中转函数
 * @param fn    待柯里化的原函数
 * @param len   所需的参数个数
 * @param args  已接收的参数列表
 */
function _curry(fn,len,...args) {
    return function (...params) {
        let _args = [...args,...params];
        if(_args.length >= len){
            return fn.apply(this,_args);
        }else{
            return _curry.call(this,fn,len,..._args)
        }
    }
}

Давайте проверим:

let _fn = curry(function(a,b,c,d,e){
    console.log(a,b,c,d,e)
});

_fn(1,2,3,4,5);     // print: 1,2,3,4,5
_fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
_fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

Наша широко используемая библиотека инструментов lodash также предоставляет метод curry и добавляет очень интересную функцию-заполнитель, которая изменяет порядок входящих параметров с помощью заполнителей.

Например, если мы передаем заполнитель, параметры, переданные в этом вызове, пропускают заполнитель, Позиция заполнителя заполняется параметрами следующего вызова, например:

Просто посмотрите на пример на официальном сайте:

Далее давайте подумаем, как реализовать функцию плейсхолдеров.

Для функции карри lodash функция карри монтируется в объект lodash, поэтому объект lodash используется в качестве заполнителя по умолчанию.

Карри-функция, которую мы реализовали сами, не монтируется ни к какому объекту, поэтому карри-функция используется как заполнитель по умолчанию.

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

Перейдите непосредственно к коду:

/**
 * @param  fn           待柯里化的函数
 * @param  length       需要的参数个数,默认为函数的形参个数
 * @param  holder       占位符,默认当前柯里化函数
 * @return {Function}   柯里化后的函数
 */
function curry(fn,length = fn.length,holder = curry){
    return _curry.call(this,fn,length,holder,[],[])
}
/**
 * 中转函数
 * @param fn            柯里化的原函数
 * @param length        原函数需要的参数个数
 * @param holder        接收的占位符
 * @param args          已接收的参数列表
 * @param holders       已接收的占位符位置列表
 * @return {Function}   继续柯里化的函数 或 最终结果
 */
function _curry(fn,length,holder,args,holders){
    return function(..._args){
        //将参数复制一份,避免多次操作同一函数导致参数混乱
        let params = args.slice();
        //将占位符位置列表复制一份,新增加的占位符增加至此
        let _holders = holders.slice();
        //循环入参,追加参数 或 替换占位符
        _args.forEach((arg,i)=>{
            //真实参数 之前存在占位符 将占位符替换为真实参数
            if (arg !== holder && holders.length) {
                let index = holders.shift();
                _holders.splice(_holders.indexOf(index),1);
                params[index] = arg;
            }
            //真实参数 之前不存在占位符 将参数追加到参数列表中
            else if(arg !== holder && !holders.length){
                params.push(arg);
            }
            //传入的是占位符,之前不存在占位符 记录占位符的位置
            else if(arg === holder && !holders.length){
                params.push(arg);
                _holders.push(params.length - 1);
            }
            //传入的是占位符,之前存在占位符 删除原占位符位置
            else if(arg === holder && holders.length){
                holders.shift();
            }
        });
        // params 中前 length 条记录中不包含占位符,执行函数
        if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
            return fn.apply(this,params);
        }else{
            return _curry.call(this,fn,length,holder,params,_holders)
        }
    }
}

Проверьте это:;

let fn = function(a, b, c, d, e) {
    console.log([a, b, c, d, e]);
}

let _ = {}; // 定义占位符
let _fn = curry(fn,5,_);  // 将函数柯里化,指定所需的参数个数,指定所需的占位符

_fn(1, 2, 3, 4, 5);                 // print: 1,2,3,4,5
_fn(_, 2, 3, 4, 5)(1);              // print: 1,2,3,4,5
_fn(1, _, 3, 4, 5)(2);              // print: 1,2,3,4,5
_fn(1, _, 3)(_, 4,_)(2)(5);         // print: 1,2,3,4,5
_fn(1, _, _, 4)(_, 3)(2)(5);        // print: 1,2,3,4,5
_fn(_, 2)(_, _, 4)(1)(3)(5);        // print: 1,2,3,4,5

На данный момент мы полностью реализовали функцию карри~~

Рекомендуемая серия статей

напиши в конце

  • Если в статье есть ошибки, исправьте их в комментариях, если статья вам поможет, добро пожаловать点赞и关注
  • Эта статья была впервые опубликована одновременно сgithub, доступны наgithubНайдите больше отличных статей вWatch & Star ★
  • Для последующих статей см.:план

Добро пожаловать в публичный аккаунт WeChat【前端小黑屋】, 1–3 высококачественные высококачественные статьи публикуются каждую неделю, чтобы помочь вам в продвижении вперед.