柯里化
柯里化(Currying)是一种将接受多个参数的函数转换为一系列接受单个参数的函数的技术。
柯里化不会调用函数。它只是对函数进行转换。
简单来说,柯里化是一种函数的转换,原本一个函数需要一次性接收多个参数才能执行,通过柯里化,将其改造成可以逐步接收参数,并在接收完所有必要参数后才执行最终的操作。
比如将一个函数从可调用的 f(a, b)
转换为可调用的 f(a)(b)
。
创建一个辅助函数 curry(func)
,该函数将对两个参数的函数 sum
执行柯里化。换句话说,对于两个参数的函数 sum(a, b)
执行 curry(func)
会将其转换为以 sum(a)(b)
形式运行的函数:
// 非柯里化的写法
function sum(a, b) {
return a + b;
}
// 将函数sum 进行 柯里化 转换
function curry(func) {
return function(a) {
return function(b) {
return func(a, b);
};
};
}
// 具体使用
let curriedSum = curry(sum);
console.log(curriedSum(1)(2)); // 3
实现原理:
curry(func)
的结果就是一个包装器function(a)
。- 当它被像
curriedSum(1)
这样调用时,它的参数会被保存在词法环境中,然后返回一个新的包装器function(b)
。 - 然后这个包装器被以
(2)
为参数调用,并且,它将该调用传递给原始的sum
函数。
柯里化更高级的实现,例如 lodash 库的 _.curry,会返回一个包装器,该包装器允许函数被正常调用或者以部分应用函数(partial)的方式调用:
function sum(a, b) {
return a + b;
}
let curriedSum = _.curry(sum); // 直接使用来自 lodash 库的 _.curry
// 以常规函数传参的形式被调用
console.log( curriedSum(1, 2) ); // 3
// 以部分应用函数的方式调用
console.log( curriedSum(1)(2) ); // 3
柯里化的优点
- 延迟计算:可以根据需要逐步传递参数,实现按需计算。
- 函数复用:通过柯里化,可以创建具有特定初始参数的新函数,提高了函数的复用性。
举例说明延迟计算、函数复用:
function curriedSum(a) {
return function(b) {
return a + b
};
}
// 先不计算,创建一个固定参数为 1 的计算函数
let curriedSumFunc = curriedSum(1);
// 在需要的时候进行计算
console.log(curriedSumFunc(2)); // 3
console.log(curriedSumFunc(5)); // 6
// 还可以创建很多固定参数不一样的计算函数
// 复用,创建一个固定参数为 2 的计算函数
let curriedSumFunc2 = curriedSum(2);
// 复用,创建一个固定参数为 3 的计算函数
let curriedSumFunc3 = curriedSum(3);
curriedSum
是一个柯里化函数。
curriedSum
接受一个参数 a
,然后返回一个新的函数function(b)
。这个新函数又接受一个参数 b
,并返回 a + b
的结果。
当执行 let curriedSumFunc = curriedSum(1)
时, a
的值被固定为 1
。此时,没有进行任何计算,而是返回了一个接受参数 b
的函数,并将其赋值给 curriedSumFunc
。
当执行 console.log(curriedSumFunc(2))
时,开始计算,此时 b
的值为 2
,因为之前 a
被固定为 1
,所以计算结果为 1 + 2 = 3
。
- 参数灵活:更容易处理参数数量不确定或部分参数需要提前固定的情况。
例如,有一个用于格式化和输出信息的日志函数log(date, importance, message)
。在实际项目中,此类函数具有很多有用的功能,例如通过网络发送日志(log)等:
// 常规写法
function log(date, importance, message) {
console.log(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}
// 将log()柯里化
curriedLog = _.curry(log);
// 柯里化后,正常调用curriedLog(date, importance, message)
curriedLog(new Date(), "DEBUG", "some debug");
// curriedLogNow 会是带有固定第一个参数的日志的部分应用函数,固定参数 date
// 换句话说,就是更简短的“部分应用函数(partially applied function)”或“部分函数(partial)”
let curriedLogNow = curriedLog(new Date());
// 此时,curriedLogNow第一个参数固定,使用curriedLogNow(importance, message)
curriedLogNow("INFO", "message"); // [HH:mm] INFO message
// 更进一步,为当前的调试日志(debug log)提供便捷函数
let debugLog = curriedLogNow('DEBUG');
debugNow("message"); // [HH:mm] DEBUG message
柯里化之后,我们没有丢失任何东西:log
依然可以被正常调用。
可以轻松地生成部分应用函数,例如用于生成今天的日志的部分应用函数curriedLogNow ()
。
柯里化会使得函数的调用又嵌套了多层,导致代码的可读性降低。
柯里化的高级实现
以下是一个更高级的柯里化实现示例,它可以处理具有多个参数的函数:
// func: 要转换的函数
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
在上述实现中,curry
函数接受一个要进行柯里化的函数 func
作为参数。
curried
函数是通过 curry
函数返回的包装器函数。它使用剩余参数 ...args
来接收每次调用时传入的参数。
执行方法时,有2个if分支:
- 如果传入的
args
长度与原始函数func
定义的参数长度相同或更长,就直接使用func.apply(this, args)
来执行原始函数并返回结果。 - 否则,返回一个新的函数
function(...args2)
:该函数在被调用时,它将重新应用curried
,会将新传入的参数args2
与之前保存的参数args
合并(通过args.concat(args2
)),然后再次调用curried
函数,形成递归调用,直到参数数量满足原始函数的要求。
使用示例:
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
// 以常规函数传参,正常调用
console.log( curriedSum(1, 2, 3) ); // 6
// 第一个参数柯里化
console.log( curriedSum(1)(2,3) ); // 6
// 全部参数柯里化
console.log( curriedSum(1)(2)(3) ); // 6
注意
- 柯里化要求函数具有固定数量的参数。
使用 rest 参数的函数,例如f(...args)
,不能以这种方式进行柯里化。 - 根据定义,柯里化应该将
sum(a, b, c)
转换为sum(a)(b)(c)
。
柯里化函数的高级实现 使得函数可以被多参数变体调用。