柯里化是一种处理函数的高级技术。它不仅用于 JavaScript,还用于其他语言。
柯里化是一种函数转换,它将函数从可调用为 f(a, b, c)
的形式转换为可调用为 f(a)(b)(c)
的形式。
柯里化不会调用函数。它只会转换函数。
让我们先看一个示例,以便更好地理解我们在谈论什么,然后再看实际应用。
我们将创建一个辅助函数 curry(f)
,它对一个有两个参数的 f
执行柯里化。换句话说,curry(f)
对于有两个参数的 f(a, b)
,将其转换为一个以 f(a)(b)
形式运行的函数
function curry(f) { // curry(f) does the currying transform
return function(a) {
return function(b) {
return f(a, b);
};
};
}
// usage
function sum(a, b) {
return a + b;
}
let curriedSum = curry(sum);
alert( curriedSum(1)(2) ); // 3
如你所见,实现非常简单:它只是两个包装器。
curry(func)
的结果是一个包装器function(a)
。- 当它被调用为
curriedSum(1)
时,参数保存在词法环境中,并返回一个新的包装器function(b)
。 - 然后,使用
2
作为参数调用此包装器,它将调用传递给原始的sum
。
柯里化的更高级实现,例如来自 lodash 库的 _.curry,返回一个包装器,该包装器允许函数以正常和部分方式调用
function sum(a, b) {
return a + b;
}
let curriedSum = _.curry(sum); // using _.curry from lodash library
alert( curriedSum(1, 2) ); // 3, still callable normally
alert( curriedSum(1)(2) ); // 3, called partially
柯里化?为什么?
要了解好处,我们需要一个有价值的真实示例。
例如,我们有日志记录函数 log(date, importance, message)
,它格式化并输出信息。在实际项目中,此类函数具有许多有用的特性,例如通过网络发送日志,这里我们只使用 alert
function log(date, importance, message) {
alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}
让我们对其进行柯里化!
log = _.curry(log);
之后 log
正常工作
log(new Date(), "DEBUG", "some debug"); // log(a, b, c)
…但也可以在柯里化形式中工作
log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)
现在,我们可以轻松地为当前日志制作一个便捷函数
// logNow will be the partial of log with fixed first argument
let logNow = log(new Date());
// use it
logNow("INFO", "message"); // [HH:mm] INFO message
现在 logNow
是具有固定第一个参数的 log
,换句话说,它是“部分应用函数”或简称“部分”。
我们可以更进一步,为当前调试日志制作一个便捷函数
let debugNow = logNow("DEBUG");
debugNow("message"); // [HH:mm] DEBUG message
所以
- 在柯里化之后,我们什么也没丢失:
log
仍然可以正常调用。 - 我们可以轻松地生成部分函数,例如用于今天的日志。
高级柯里化实现
如果你想了解详细信息,这里是我们可以在上面使用的多参数函数的“高级”柯里化实现。
它很短
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));
}
}
};
}
用法示例
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
alert( curriedSum(1, 2, 3) ); // 6, still callable normally
alert( curriedSum(1)(2,3) ); // 6, currying of 1st arg
alert( curriedSum(1)(2)(3) ); // 6, full currying
新的 curry
看起来可能很复杂,但实际上很容易理解。
curry(func)
调用的结果是包装器 curried
,它看起来像这样
// func is the function to transform
function curried(...args) {
if (args.length >= func.length) { // (1)
return func.apply(this, args);
} else {
return function(...args2) { // (2)
return curried.apply(this, args.concat(args2));
}
}
};
当我们运行它时,有两个 if
执行分支
- 如果传递的
args
计数与原始函数在其定义中拥有的计数相同或更多(func.length
),则只需使用func.apply
将调用传递给它。 - 否则,获取一个部分:我们现在不调用
func
。相反,返回另一个包装器,它将重新应用curried
,提供以前的参数和新参数。
然后,如果我们再次调用它,我们将获得一个新的部分(如果没有足够的参数)或最终的结果。
柯里化要求函数具有固定数量的参数。
使用剩余参数的函数(例如 f(...args)
)不能以这种方式进行柯里化。
根据定义,柯里化应将 sum(a, b, c)
转换为 sum(a)(b)(c)
。
但 JavaScript 中的大多数柯里化实现都是高级的,如下所述:它们还使函数在多参数变体中可调用。
总结
柯里化是一种转换,它使 f(a,b,c)
可作为 f(a)(b)(c)
调用。JavaScript 实现通常既保持函数正常可调用,又当参数数量不足时返回部分。
柯里化使我们能够轻松地获得部分。正如我们在日志记录示例中看到的那样,在对三个参数通用函数 log(date, importance, message)
进行柯里化后,当用一个参数(如 log(date)
)或两个参数(如 log(date, importance)
)调用时,它会给我们部分。
评论
<code>
标记,对于多行 - 将它们包装在<pre>
标记中,对于超过 10 行 - 使用沙盒(plnkr,jsbin,codepen…)