先说说为什么要说函数柯里化
刚开始有朋友给我看一个面试题 实现:add(1)(2)(3)(4)=10; 、 add(1)(1,2,3)(2)=9;
,看到这个面试题我内心是抗拒的,为什么要把这些参数分开写呢?这个面试官是不是沙?大家请记住上面这句话,后面会疯狂打脸。当时我已经听说过函数柯里化,但是对柯里化一无所知。
后来在自己喜欢的公众号上看到有人发这个题目,才感觉到这个面试题并不是那么简单,没想到打脸来的如此之快,(罗老师对不起了)
上面试题
实现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函数,带着这个疑问,我又多方面看掘金博文,终于有一篇文章告诉了我,柯里化的作用
拨云见雾
感谢掘金作者云中桥
看了文章后,感觉柯里化真香
这篇文章有几个例子,我刚开始没看懂,直到我看到这个例子才知道柯里化的作用
柯里化实际是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度。 而这里对于函数参数的自由处理,正是柯里化的核心所在。 柯里化本质上是降低通用性,提高适用性。来看一个例子:
我们工作中会遇到各种需要通过正则检验的需求,比如校验电话号码、校验邮箱、校验身份证号、校验密码等, 这时我们会封装一个通用函数 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函数
- 我刚开始想的是本文最上面的add()方法,但是发现并不可以,因为这个例子把sum函数提取出来了,然后我尝试了很久最终也没能解决
- 所以根据答案来反推,上答案
/**
* 将函数柯里化
* @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)
}
}
}
验证后是没问题的 那我们来写一下思路
- 总的来说是收集参数,返回一个函数接收剩余参数,接收到足够的参数后,执行原函数,否则返回curry继续收集参数
- 通过函数的length属性(函数的length可以获取形参的个数),获取形参的个数,当接收到的参数等于形参的个数的时候
- 调用函数fn,并且把收集到的参数传给fn作为实参
- 整个过程虽然简单的几句话,但是这是写了思考了整整一天+敲代码+看云中桥笔记总结而来
- 建议大家要手敲几遍
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函数对于这个例子也是可以的
至此,我对函数柯里化有了一定的了解,但是还有占位符的方式来改变传入参数的顺序,这个有时间再更新……