从一道编程题理解函数柯里化
柯里化是函数的一个高级应用,平常使用少,理解起来也比较困难。但是在一些面试中经常会问到,因此我希望放弃对柯里化的常规理解,而是通过一道面试题来理解它。
从一道编程题目开始
题目:有这样一个函数add(1,2,3)它实现的功能是返回函数所有参数的相加的和。现在需要你定义一个函数fn,只能接收一个参数,但是实现和add函数同样的功能,也就是说fn(1)(2)(3)执行的结果和add(1,2,3)相同。
首先我们分析一下,fn的函数特点:
fn(1)(2)(3)接收参数3,说明fn(1)(2)返回的是一个函数,这个函数接受一个参数。
fn(1)(2)接收参数2,说明fn(1)返回一个函数,这个函数接收一个参数。
根据上面的分析,我们可以知道fn实际上是一个这样类型的函数:每次接收一个参数,然后返回一个参数,直到最后进行计算。
const fn = function (a){
return function(b){
return function(c){
return add(a+b+c);
}
}
}
上面的函数add(1,2,3)只需要计算3个参数的和,假设有一个函数add(1,2,3,4)需要计算4个参数的和,那么同样的实现方法:
const fn = function (a){
return function(b){
return function(c){
return function(d){
return add(a+b+c+d)
}
}
}
}
如果有更多的参数,那么我们就需要返回更多次数的函数,毫无疑问我们需要对其进行封装。
封装currify函数
const currify = (fn,params = []) => {
return (p) => {
params.push(p);
// 一种情况是再次返回一个函数,还有一种情况是返回一个计算的结果
if(params.length===fn.length){ // fn.length是函数参数的个数。
return fn(...params);
}else{
return currify(fn,params);
}
}
}
我们定义一个柯里化函数,这个函数接受一个参数fn,这个fn是最终处理数据的。同时接受一个参数数组用来存储每次收集的参数(由于每次只收集一个参数,而且不处理参数,因此需要将参数保存起来,交给最终的处理函数fn处理)。
我们使用定义的currify函数来分别处理2个,3个,4个参数的求和。发现结果与原来的计算一致。
const _addTwo = currify(addTwo)
_addTwo(1)(2);
const _addThree = currify(addThree);
_addThree(1)(2)(3));
currify函数支持任意个参数
如果想要支持任意个参数,由于不知道每次传参的个数,也就是说无法获取fn.length,因此我们不知道什么时候递归结束,因此我们不能使用上面我们封装的currify的通用公式来转换一个柯里化函数。那么应该如何办了?
支持任意个参数的核心是我们必须根据是否还有参数来判断是否继续递归,而不能根据参数的长度来确定,因此我们可以始终返回一个函数,在函数调用中继续递归。如果没有参数了,函数不执行,那么就不会递归了。
function currify() {
// 第一次执行时定义一个数组专门用来保存参数:
let params = Array.prototype.slice.call(arguments);
return (...args) => {
params = [...params, ...args];
return currify(...params); // 当有参数调用时才会继续调用currify,因此如果没有参数了这里不会执行。
}
}
但是,这样的话我们最终返回的是一个函数,函数是无法参与计算得到值的。这时候我们需要了解一下函数的隐士转换。
函数的隐式转换
当我们直接将函数参与其他的计算时,函数会默认调用toString方法,直接将函数体转换为字符串参与计算
function fn(){
return 1;
}
console.log(fn + 10);
最终得到的结果是:
"function fn() {
return 20;
}10"
我们可以重写函数的toString方法,让函数参与计算时,输出我们想要的结果。
function fn() { return 20; }
fn.toString = function() { return 20 }
console.log(fn + 10); // 30
最终版
利用函数的隐式转换进行计算,得到我们想要的值。
function currify() {
// 第一次执行时定义一个数组专门用来保存参数:
let params = Array.prototype.slice.call(arguments);
let fn = (...args) => {
params = [...params, ...args];
return currify(...params); // 当有参数调用时才会继续调用currify,因此如果没有参数了这里不会执行。
}
// 改写函数的toString方法。只有函数参与运算时才会执行。
fn.toString = () => {
return params.reduce( (a, b) => {
return a + b;
});
}
return fn
}
使用上面的currify函数进行计算:
const result1 = addCurrify(1)(2)(3); // fnction 6
const result2 = addCurrify(1,2)(3,4)(5); // fnction 15
console.log(result1 + 4); // 10
console.log(result2.toString()); // 15
总结
我们从一个简单的一道编程题出发,最终实现了一个参数非常灵活的currify函数,这个函数支持以下这些传参方式(以3个参数为例):
currify(1,2,3)
currify(1)(2)(3)
currify(1,2)(3)
currify(1)(2,3)
也就是说上面的所有函数是等价的。这就是函数的柯里化。因此,在我看来函数的柯里化就是把原来只支持一种传参的函数转化成能够非常灵活传参的方式,这种灵活的传参能够在开发中给我们带来一些便利。但是函数的柯里化终究是函数的一种高级用法,感觉开发中使用频率非常低,因此究竟能够在哪些场景下使用可以参考其他的大神的文章。