柯里化原理
把一个多参函数转化为单参函数
输入输出
输入
一个函数
输出
两种情况:
- 输入函数的参数没有传齐时都是返回 一个函数。
- 输入函数的参数传齐时,返回输入函数的执行结果。
注意点
函数的length属性
返回函数参数的长度
function a(a,b,c){
return a+b+c;
};
a.length; // 3
实现
1. 普通函数的另外一种调用方式
改变了传参的方式(参数都传齐时,直接调用fn函数,并把返回函数的参数给到fn)
针对的是,直接在curry包装后的函数,把参数都传给他,在本函数内,就是做了一个参数的传递,把curry(fn)(...args),中的args传递给fn函数。
function curry(fn){
if(fn.length <=1){return fn}
var returnFn = (...args) => {
if(fn.length === args.length){
//参数都传齐时,直接调用fn函数,并把返回函数的参数给到fn
return fn(...args)
}else{
//参数没传齐时,打印传入的参数。
console.log(args)
}
}
return returnFn
}
var add = (a,b,c,d) => a+b+c+d;
var curriedAdd = curry(add);
curriedAdd(1,2,3); // 打印出 [1, 2, 3]
分析
- curry后返回一个函数。
- 给返回的这个函数传递参数
- 当returnFn的参数个数与输入函数fn的参数个数相同时
- 调用fn函数并把returnFn的参数给到fn。
- 返回上一步的调用结果。也就是相当于换了一种方式调用输入函数。
add调用方式对比:
普通函数调用:
var add = (a,b,c,d) => a+b+c+d;
add(1,2,3,4)
用curry包装后的调用方式
var add = (a,b,c,d) => a+b+c+d;
var returnFn = curry(add);
returnFn(1,2,3,4)
- 不直接调用add。而是把add当做一个参数,传给另外一个函数curry。
- 返回一个函数returnFn。
- 把参数传给返回的函数。
- 在返回的函数内部返回调用的了fn。
- 整体相当于curry(add)(1,2,3,4) , curry第一个参数是我们要调用的函数。返回函数的参数是我们要调用函数的参数。
2. 完整版的柯里化(es6的版本)(递归+参数拼接)
参数的拼接方式,比较灵活。建议使用此方法
function curry(fn){
if(fn.length <=1){return fn}
var returnFn = (...args) => {
//console.log(...args,typeof ...args);
if(fn.length === args.length){
//参数都传齐时
return fn(...args)
}else{
//参数没传齐时,就返回一个函数
return (...args2) => {
console.log(args2," ",args);
return returnFn(...args,...args2)
}
}
}
return returnFn
}
var add = (a,b,c,d) => a+b+c+d;
//包装add
var returnFn = curry(add);
// 递归传递参数给returnFn
var returnFnArrowFn = returnFn(1)(2)(3);
// 参数传齐,returnFn将参数传递给输入函数fn, 并调用fn
returnFnArrowFn(4);// 10
递归调用 returnFn 时的参数传递
如上代码中的 console.log(args,args2) 的打印结果
console.log(args,args2);
// [1] [2]
// [1,2] [3]
// [1,2,3] [4]
// 此时,参数也传齐,走上面的if分支。调用输入函数fn。
args 是 returnFn的参数。 args2 是 returnFn 中匿名函数的参数。
分析
- (参数<=1时,原样输出)判断输入函数的参数个数,如果参数个数<=1,那么直接返回输入函数
- 定义返回的函数,并返回
- (返回的函数把参数传齐时)返回函数的入参个数判断,如果入参个数等于输入函数fn的参数个数,那么,说明参数都已传齐,直接调用输入函数fn,并把返回函数的参数传给输入函数。(因为我们最终的目的就是调用输入函数,参数当然也都是给fn用的)
- (参数没传齐时),永远都是返回一个函数。
- 返回的这个函数的参数在传递给returnFn函数。
- 递归调用returnFn函数
- 直到参数都从最里层的返回函数传递给returnFn函数。returnFn函数的参数在传递给输入函数。
- 注意:只有return函数的参数个数和输入函数的参数个数相同时,才会传递给输入函数。并调用输入函数,并返回。
画图分析

3. 实现方式三(es5的版本)(递归+参数拼接)
es5的版本,参数拼接方式比较繁琐。
function curry(fn, args=[]) {// 参数args不建议传入
length = fn.length;
// args = args || [];
return function() {
//复制curry中的第二个参数数组到_args
var _args = args.slice(0),
arg, i;
for (i = 0; i < arguments.length; i++) {
arg = arguments[i];
// 把返回函数的参数 push 到 _args中。
// _args用于存储 所有的 不断传入的参数。
_args.push(arg);
}
if (_args.length < length) {
// 当参数不齐时,递归调用自身,并把现有的拼接好的参数传递给curry。
// 直到 参数齐时,调用输入函数fn,并传入所有拼接好的参数。
return curry.call(null, fn, _args);
}
else {
// curry函数的最后执行的一步,参数已传齐,开始调用fn。
return fn.apply(null, _args);// 匿名函数中的this。指向的是window。没必要修改this。
}
}
}
var returnFn = curry(function(a, b, c) {
return a+b+c;
});
var nextFn = returnFn(1)(2);// 参数不齐,返回匿名函数,收集参数
nextFn(3); // 6 // 参数已齐,调用curry,的fn,真正求值。
分析
- 借助curry函数的第二个参数来,不断的收集后续传入的参数。
- 借助_args变量来连接参数和新传入的参数
- 递归调用curry,不断的传入连接好的参数。
- 直到,_args.length === length, 传入的参数总数和curry包装的函数fn的参数个数相同时
- 调用fn,并传入所有连接好的参数。
- 返回fn的执行结果。
参考
应用场景
- ajax
- 代码简化
- ramdajs, underscorejs, lodash 等工具库都使用了函数式的思想
- 比如,pipe, compose, cond, R.ifElse , 等等
ajax的柯里化场景
curry 的这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性
// 示意而已
function ajax(type, url, data) {
var xhr = new XMLHttpRequest();
xhr.open(type, url, true);
xhr.send(data);
}
// 虽然 ajax 这个函数非常通用,但在重复调用的时候参数冗余
ajax('POST', 'www.andy.com', "name=andy")
ajax('POST', 'www.andy.com', "name=andy")
ajax('POST', 'www.andy.com', "name=andy")
// 利用 curry
var ajaxCurry = curry(ajax);
// 以 POST 类型请求数据
var post = ajaxCurry('POST');
post('www.andy.com', "name=andy");
// 以 POST 类型请求来自于 www.andy.com 的数据
var postFromTest = post('www.andy.com');
postFromTest("name=andy");
对象取值的柯里化场景
普通写法
var person = [{name: 'andy'}, {name: 'amy'}];
我们取person中的name,代码怎么写呢?
var nameArr = person.map((item,index) => {
return item.name
});
console.log(nameArr); // ["andy", "amy"]
通过ramdajs 我们怎么写呢?
var person = [{name: 'andy'}, {name: 'amy'}];
R.map(R.prop('name'))(person); // ["andy", "amy"]
通过 Lodash 如何取呢?
var person = [{name: 'andy'}, {name: 'amy'}];
_.map(person,'name'); // ["andy", "amy"]
通过 underscore 如何取呢?
var person = [{name: 'andy'}, {name: 'amy'}];
_.map(person,(item)=>item.name); // ["andy", "amy"]
或者:
_.map(person,['name']); // ["andy", "amy"]
以上代码ramdajs 和 lodash,underscore。的区别,明显的是数据的传递方式不一样。 ramdajs是数据传递放后面。而后两者是数据传前面。
柯里化用途小结
- 以上第一个例子,是参数复用
- 第二个例子是,取数组对象中的某个属性,把三行代码简写成一行代码
- R.map(R.prop('name'))(person); 代码的可读性与提高了。翻译成中文是,person对象(map)遍历,取属性(prop) 'name'
就像读英文句子一样去读代码。
总结
使用了几年的函数式库,把内部的重要理念形成文章。回顾一下,也希望对读到文章的同学有所用。共同深入。
- 柯里化,直白的说,就是包装一个普通函数,换另外一种方式调用这个普通函数
- 普通函数被curry化后,将更加灵活。复用性更强
- 柯里化的好处一:参数复用
- 还有函数式的其他特点:高阶函数,纯函数(幂等函数),后续我们在一一介绍。
- 提高代码的可测试性,可读性,代码的健壮性。
curry的实现,最关键的就是 返回函数 + 递归 + 参数的传递 + 真正调用输入函数