我终于了解了函数柯里化

4,341 阅读8分钟

先说说为什么要说函数柯里化

刚开始有朋友给我看一个面试题 实现:add(1)(2)(3)(4)=10; 、 add(1)(1,2,3)(2)=9; ,看到这个面试题我内心是抗拒的,为什么要把这些参数分开写呢?这个面试官是不是沙?大家请记住上面这句话,后面会疯狂打脸。当时我已经听说过函数柯里化,但是对柯里化一无所知。

后来在自己喜欢的公众号上看到有人发这个题目,才感觉到这个面试题并不是那么简单,没想到打脸来的如此之快,d947d7d18e0a6c5399e9d8157f0b51c0.gif(罗老师对不起了)

上面试题

实现add(1)(2)(3)(4)=10; 、 add(1)(1,2,3)(2)=9;

#title

function add() {
  const _args = [...arguments];
  function fn() {
    _args.push(...arguments);
    return fn;
  }
  fn.toString = function() {
    return _args.reduce((sum, cur) => sum + cur);
  }
  return fn;
}

如果大家没有接触过柯里化,上面的面试题要看一会,我说一下思路(建议大家一定要写一写)

// 思路:
// 1、函数要返回一个函数(我是否可以先声明一个函数,然后返回)
// 2、如果add没有参数了,那我就直接返回累加即可
// 3、我怎么知道add没有参数了呢?先要收集参数吧(多次收集)
// 4、还要在函数fn内部返回fn(这里可能会想这个会不会死循环,答案是不会,因为返回的是fn而不是让fn()执行)
// 5、这个时候打印的是字符串的函数fn,其实是fn.toString 的结果,所以可以 写return _args.reduce((acc, cur) => acc + cur)
function add() {
  const _args = [...arguments] // 3、我怎么知道add没有参数了呢?先要收集参数吧,收集第一次的参数
  function fn() { // 1、函数要返回一个函数(我是否可以先声明一个函数,然后返回)
    _args.push(...arguments) // 3、收集第二次的参数
    return fn // 4、还要在函数fn内部返回fn(这里可能会想这个会不会死循环,答案是不会,因为返回的是fn而不是让fn()执行)
  }
  fn.toString = function() {
    return _args.reduce((acc, cur) => acc + cur)
  }
  return fn // 1、函数要返回一个函数(我是否可以先声明一个函数,然后返回)
}
console.log(add(1)(2)(3)(4))
console.log(add(1)(1, 2, 3)(2))

如果大家打印的结果是个函数可以打印console.log(add(1)(2)(3)(4).toString())console.log(add(1)(1, 2, 3)(2)).toString() 解决了这个面试题,倍感欣慰,然后什么是柯里化,柯里化的作用是什么?我依旧不明白

柯里化的学习之路

什么是柯里化?

指的是将一个接受多个参数的函数 变为 接受一个参数返回一个函数的固定形式,这样便于再次调用

看了柯里化的定义,我依旧不懂,柯里化有什么应用场景吗?所以我懒惰性的去B站看了一个人讲的视频又有一道面试题,话不多说,上题目

const namelist1 = [
  { mid: '哈傻k', profession: '中单' },
  { mid: '沙皇', profession: '中单' },
  { mid: '卡牌', profession: '中单' },
  { mid: '发条', profession: '中单' },
]
const namelist2 = [
  { adc: '轮子妈', profession: 'ADC' },
  { adc: 'VN', profession: 'ADC' },
  { adc: '老鼠', profession: 'ADC' },
]
// 普通写法
// console.log(namelist1.map(hero => hero.mid))
// console.log(namelist2.map(hero => hero.adc))
// 柯里化写法
// const curry = name => element => element[name]
// const mid = curry('mid')
// const adc = curry('adc')
// console.log(namelist1.map(mid))
// console.log(namelist2.map(adc))

WTF 这是什么?有必要吗?普通写法不是好的很,为什么我要多此一举的写一个curry函数,带着这个疑问,我又多方面看掘金博文,终于有一篇文章告诉了我,柯里化的作用

拨云见雾

感谢掘金作者云中桥 看了文章后,感觉柯里化真香src=http___img.111com.net_attachment_art_167859_6601058754.gif&refer=http___img.111com.gif 这篇文章有几个例子,我刚开始没看懂,直到我看到这个例子才知道柯里化的作用

柯里化实际是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度。 而这里对于函数参数的自由处理,正是柯里化的核心所在。 柯里化本质上是降低通用性,提高适用性。来看一个例子:

我们工作中会遇到各种需要通过正则检验的需求,比如校验电话号码、校验邮箱、校验身份证号、校验密码等, 这时我们会封装一个通用函数 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'); // 校验邮箱

以上代码是摘抄云中桥作者 此时我才明白柯里化的作用,那么我们再回过头去看上面B站那道题目

const namelist1 = [
  { mid: '哈傻k', profession: '中单' },
  { mid: '沙皇', profession: '中单' },
  { mid: '卡牌', profession: '中单' },
  { mid: '发条', profession: '中单' },
]
const namelist2 = [
  { adc: '轮子妈', profession: 'ADC' },
  { adc: 'VN', profession: 'ADC' },
  { adc: '老鼠', profession: 'ADC' },
]
// 普通写法
// console.log(namelist1.map(hero => hero.mid))
// console.log(namelist2.map(hero => hero.adc))
// 柯里化写法
// const curry = name => element => element[name]
// const mid = curry('mid')
// const adc = curry('adc')
// console.log(namelist1.map(mid))
// console.log(namelist2.map(adc))
当初看到这个代码感觉多此一举,为了得到一个属性值,还要封装一个curry,
还声明了mid和adc,这样也太麻烦了

但是我们想一下,mid是可以多次使用的如果再遇到需要拿属性名为mid的数组,
也就可以直接写namelist1.map(mid)

这样看,通过柯里化的方式,我们的代码是不是更精简了,
而且我们也知道要拿的是mid的属性(第二次就知道了)

但是云中桥的文章里还有几个例子我们一一来看

1. 例子一

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

//一个接收三个参数的普通函数
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

我们开始来实现curry函数

  1. 我刚开始想的是本文最上面的add()方法,但是发现并不可以,因为这个例子把sum函数提取出来了,然后我尝试了很久最终也没能解决
  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)
        }
    }
}

验证后是没问题的 那我们来写一下思路

  1. 总的来说是收集参数,返回一个函数接收剩余参数,接收到足够的参数后,执行原函数,否则返回curry继续收集参数
  2. 通过函数的length属性(函数的length可以获取形参的个数),获取形参的个数,当接收到的参数等于形参的个数的时候
  3. 调用函数fn,并且把收集到的参数传给fn作为实参
  4. 整个过程虽然简单的几句话,但是这是写了思考了整整一天+敲代码+看云中桥笔记总结而来
  5. 建议大家要手敲几遍

2. 例子二

//普通函数
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

上面的curry函数对于这个例子也是可以的

3. 例子三

function checkByRegExp(regExp,string) {
    return regExp.test(string);  
}
//进行柯里化
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'); // 校验邮箱

上面的curry函数对于这个例子也是可以的

至此,我对函数柯里化有了一定的了解,但是还有占位符的方式来改变传入参数的顺序,这个有时间再更新……