最近面试的时候遇到了一个面试题:add(1)(2)(3)(4)输出结果为10。
const addFn = (...args) => args.reduce((total, cur) => total + cur, 0)
const curry = (fn) => {
// Your Code Here
}
const add = curry(addFn)
const value = add(1,2)(3)(4)()
console.log(value) // 10
看到这道面试题的时候,有点迷茫,不知所措~~~
就像是宝强在《人在囧途》中的反应:啥!啥!啥!这写的都是啥?
一开始,我发现1+2+3+4=10,写了以下的代码
function add (a, b, c, d) {
return a + b + c + d
}
add(1, 2, 3, 4) // 10
add(1,2)(3)(4)()//10
function add (a) {
return function (b) {
return function (c) {
return function (d) {
return a + b + c + d
}
}
}
}
又觉得不会这么简单吧,要是累加到100、1000呢~
没有思路~
面试结束后,在网上找相关的知识点学习,了解到一个概念叫:函数柯里化。
什么是函数柯里化(curry)
函数柯里化(curry)是函数式编程里面的概念。
curry的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
简单点来说就是:每次调用函数时,它只接受一部分参数,并返回一个函数,直到传递所有参数为止。
举个🌰 将下面接受两个参数的函数改为接受一个参数的函数。
const add = (x, y) => x + y;
add(1, 2);
改成每次只接受一个参数的函数
const add = x => y => x + y;
add(1)(2);`
我们可以自己先尝试写一个add(1)(2)(3)
const add = x => y => z => x + y + z;
console.log(add(1)(2)(3));
看起来并不是那么难,但是如果面试官的要求是实现一个add 函数,同时支持下面这几种的用法呢
add(1, 2, 3);
add(1, 2)(3);
add(1)(2, 3);
如果还是按照上面的这种思路,我们是不是要写很多种呢...
我们当然可以自己实现一个工具函数专门来生成柯里化函数。
主要思路是什么呢,要判断当前传入函数的参数个数 (args.length) 是否大于等于原函数所需参数个数 (fn.length) ,如果是,则执行当前函数;如果是小于,则返回一个函数。
const curry = (fn, ...args) =>
// 函数的参数个数可以直接通过函数数的.length属性来访问
args.length >= fn.length // 这个判断很关键!!!
// 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
? fn(...args)
/**
* 传入的参数小于原始函数fn的参数个数时
* 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
*/
: (..._args) => curry(fn, ...args, ..._args);
function add1(x, y, z) {
return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));
柯里化有什么作用
主要有3个作用: 参数复用、提前返回和 延迟执行
我们来简单的解释一下: 参数复用:拿上面 f
这个函数举例,只要传入一个参数 z
,执行,计算结果就是 1 + 2 + z
的结果,1 和 2 这两个参数就直接可以复用了。
提前返回 和 延迟执行 也很好理解,因为每次调用函数时,它只接受一部分参数,并返回一个函数(提前返回),直到(延迟执行)传递所有参数为止。
函数柯里化解决方案
函数柯里化有两种不同的场景,一种为函数参数个数定长的函数,另外一种为函数参数个数不定长的函数。
函数参数个数定长的柯里化解决方案
// 定长参数
function add (a, b, c, d) {
return [
...arguments
].reduce((a, b) => a + b)
}
function currying (fn) {
let len = fn.length
let args = []
return function _c (...newArgs) {
// 合并参数
args = [
...args,
...newArgs
]
// 判断当前参数集合args的长度是否 < 目标函数fn的需求参数长度
if (args.length < len) {
// 继续返回函数
return _c
} else {
// 返回执行结果
return fn.apply(this, args.slice(0, len))
}
}
}
let addCurry = currying(add)
let total = addCurry(1)(2)(3)(4) // 同时支持addCurry(1)(2, 3)(4)该方式调用
console.log(total) // 10
函数参数个数不定长的柯里化解决方案
问题升级:那这个问题再升级一下,函数的参数个数不确定时,如何实现呢?
function add (...args) {
return args.reduce((a, b) => a + b)
}
function currying (fn) {
let args = []
return function _c (...newArgs) {
if (newArgs.length) {
args = [
...args,
...newArgs
]
return _c
} else {
return fn.apply(this, args)
}
}
}
let addCurry = currying(add)
// 注意调用方式的变化
console.log(addCurry(1)(2)(3)(4, 5)())
函数柯里化是一种技术,一种将多入参函数变成单入参函数。
这样做会让函数变得更复杂,但同时也提升了函数的普适性。
举个例子
//正常函数
function sum(a,b){
console.log(a+b);
}
sum(1,2); //输出3
sum(1,3); //输出4
//柯里化函数
function curry(a){
return (b) =>{
console.log(a+b)
}
}
const sum = curry(1);
sum(2); //输出3
sum(3); //输出4
例子里,为使用柯里化的函数在入参的时候即使在某一个入参是固定的情况下。也需要一样的去输入,那么这个输入就变得冗余了。
柯里化之后的函数可以省略掉一个固定的入参。
但到这里,还有一个问题。现在只是一层封装的柯里化。如果是四层,五层呢。
假设有这样一个场景
//柯里化之前
function sum(a,b,c,d,e){
console.log(a+b+c+d+e)
}
sum(1,2,3,4,5);
//柯里化
function sum1(a){
return function sum2(b){
return function sum3(c){
return function sum4(d){
return function sum5(e){
console.log(a+b+c+d+e)
}
}
}
}
}
sum1(1)(2)(3)(4)(5);
多层柯里化的时候代码会不美观,可读性非常差。
但需求总是在的。我们总会需要多层柯里化的时候。
所有,我们可以封装一个函数来帮助我们完成函数向柯里化的转换。
//函数柯里化封装(这个封装可以直接复制走使用)
function curry(fn, args) {
var length = fn.length;
var args = args || [];
return function () {
newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < length) {
return curry.call(this, fn, newArgs);
} else {
return fn.apply(this, newArgs);
}
}
}
//需要被柯里化的函数
function multiFn(a, b, c) {
return a * b * c;
}
//multi是柯里化之后的函数
var multi = curry(multiFn);
console.log(multi(2)(3)(4));
console.log(multi(2, 3, 4));
console.log(multi(2)(3, 4));
console.log(multi(2, 3)(4));
柯里化的应用场景
其实柯里化大多是情况下是为了减少重复传递的不变参数。
举个最简单的例子吧。手机号正则校验。
//校验手机号
function validatePhone(regExp,warn,phone){
const reg = regExp;
if (phone && reg.test(phone) === false) {
return Promise.reject(warn);
}
return Promise.resolve();
}
//调用校验
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符",187****3311)
这种写法乍一看好像没什么问题。但是,如果你需要多次调用呢?
//调用校验
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符",137****1234)
//调用校验
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符",159****6204)
//调用校验
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符",137****2125)
//调用校验
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符",191****5236)
会发现,正则和提示入参是固定的。很冗余。
我们可以使用我们上面封装的柯里化工具(curry函数)进行如下修改。
//完成柯里化
const curryValid = curry(validatePhone);
const validatePhoneCurry =curryValid(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符");
//调用柯里化之后的函数
validatePhoneCurry(159****6204);
validatePhoneCurry(137****1234);
validatePhoneCurry(137****2125);
validatePhoneCurry(191****5236);
如上,我们可以省略很多不必要的参数。
当然,柯里化的应用场景还有延时执行(闭包也可以实现,而且更简单),还有提前返回(主要针对IE,IE也马上退休了,这里不认为有赘述的意义)