在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
基本实现
简单点来说,就是允许函数不一次性接收所有参数,而是接收部分参数后返回一个接收剩余参数的函数。
function sum(num1, num2) {
return num1 + num2
}
function add(num2) {
return sum(5, num2)
}
console.log(sum(2, 3)) // 5
console.log(add(3)) // 8
上面这个例子定义了两个函数sum()和add(),后者add本质上是前者sum第一个参数固定为5的版本。虽然add并不是柯里化函数,但是展示了其基本概念。
我们把函数改进一下:
function sum(num1) {
return function(num2) {
return num1 + num2
}
}
console.log(sum(2)(3)) // 5
首先定义了一个add函数,它接受一个参数并返回一个新的函数。调用add之后,num1通过闭包的方式保存在内存,返回的是一个匿名函数。传入第二个参数后,匿名函数调用内存中的第一个参数(作用域),返回结果。
也可以用ES6的方法简写
const sum = a => b => a+b
console.log(sum(2)(3)) // 5
持续改进
这里以一道面试题来作为范例:
如何实现add(1)(2)(3) = 6
按照上面的例子,我们可以很轻松的写出:
add = a => b => c => a+b+c
console.log(add(1)(2)(3)) //6
虽然实现了函数的柯里化,但是这样一层一层的嵌套函数不够优雅,可拓展性也太差。那么能不能通过一个专门的函数来让目标函数柯里化呢?
const sum = (a, b, c) => a+b+c
console.log(sum(1,2,3)) // 6
//库里函数
function curry(fn){
//获取传入的除了第一项(被柯里化的函数)的参数
let args = Array.prototype.slice.call(arguments, 1) //[1]
return function() {
let innerArgs = Array.prototype.slice.call(arguments) //[2,3]
let newArgs = args.concat(innerArgs) //[1,2,3]
//返回函数,将所有参数传入
return fn.apply(null,newArgs)
}
}
const add = curry(sum,1)
console.log(add(2,3)) // 6
虽然能分开传入参数,这样好像和题目不一样啊?不能一个个分开传参数,写这么多代码干嘛?
问题出在哪儿呢?以上面的为例,当我们传入第一个参数,能够获取这个参数[1],并返回一个函数,但是第二个第三个参数,却需要一起传入[2,3]。这里我们需要做一个判断,当参数没传完,继续柯里化返回的函数(递归),当所有参数传完了,返回结果。
const sum = (a, b, c) => a+b+c
function curry(fn,args){
//获取传入函数的参数的个数
let length = fn.length;
//存储传入的参数,最开始为空数组
let innerArgs = args || [];
//为了直观,打印了值,依次为[] [1] [1,2] [1,2,3]
console.log(innerArgs)
return function() {
let newArgs = innerArgs.concat(Array.prototype.slice.call(arguments))
//为了直观,打印了值,依次为[1] [1,2]
console.log(newArgs)
//做判断,如果存储的参数少于函数的参数,那么再次处理函数
if(newArgs.length < length) {
return curry.call(this, fn, newArgs)
}else {
return fn.apply(null, newArgs)
}
}
}
const add = curry(sum)
console.log(add(1)(2)(3)) // 6
console.log(add(1,2)(3)) // 6
console.log(add(1)(2,3)) // 6
这样,我们不仅满足了题目的要求,还更进一步了!
简化
虽然实现了要求,但是代码过于繁琐,能不能更简单些呢?当然!
抽离出柯里化的逻辑,就是判断传入函数的参数个数,不够就固定参数返回函数,传完了就将所有参数填进去返回,那么我们可以这样写:
const sum = (x,y,z) => x+y+z
console.log(sum(1,2,3)) // 6
const curry = (fn, parmas) => {
//初始化参数数组
if(!Array.isArray(parmas)){
parmas = []
}
//ES6获取所有参数
return function(...parameter){
//判断参数是否传完
if(parmas.length + parameter.length < fn.length) {
return curry(fn,parmas.concat(parameter))
}
//参数传完就返回结果
return fn.apply(null,parmas.concat(parameter))
}
}
const cSum = curry(sum)
console.log(cSum(1)(2)(3)) // 6
console.log(cSum(1,2)(3)) // 6
作用
使用柯里化大致有三个方面参数复用、延迟计算、提前返回。
在上面的例子中,如果a的值一直是2,只改变b的值,那么使用普通函数
sum(2, 10)
sum(2, 8)
sum(2, 3)
而在柯里化之后,就像上面的例子中写的
sum = a => b => a+b
let add2 = sum(2)
console.log(add2(10)) // 12
而在这个过程中,如果使用柯里化前的代码,或当即就把结果计算出来,而在柯里化之后,我们可以现传入一个2,然后在想得到真正结果的时候再传入另一个参数,体现了延迟计算。
同时柯里化函数还可以 提前返回,很常见的一个例子,兼容现代浏览器以及IE浏览器的事件添加方法。我们正常情况不使用柯里化可能会这样写:
const addEvent = function(element, type, fn, capture) {
if (window.addEventListener) {
element.addEventListener(type, function(e) {
fn.call(element, e);
}, capture);
} else if (window.attachEvent) {
element.attachEvent("on" + type, function(e) {
fn.call(element, e);
});
}
};
上面的方法有什么问题呢?我们每次使用addEvent为元素添加事件的时候都会走一遍if...else其实只要一次判定就可以了。
const addEvent = (function(){
if (window.addEventListener) {
return function(element, type, fn, capture) {
element.addEventListener(type, function(e) {
fn.call(element, e);
}, (capture));
};
} else if (window.attachEvent) {
return function(element, type, fn, capture) {
element.attachEvent("on" + type, function(e) {
fn.call(element, e);
});
};
}
})();
总结
这是一个日常工作中很少用到的知识,去搜索柯里化大多为了应付一些面试题,但是了解柯里化,进一步了解函数式编程,对编写可维护性代码有一定的帮助,多学一点总是好的,继续加油。