当我们谈Currying时,我们在谈些什么
作者:互联网
在数学中,函数是集合到集合的映射,即集合 A
中的元素,经过处理后,映射到集合 B
中的元素。可以简单表示如下:
f(A)->B
函数式编程,顾名思义,就是用函数的方式去编程。关于函数式编程的起源可以追溯到20世纪初的λ演算。在λ演算中,函数只有一个参数,为了实现多参数,于是出现了函数的柯里化(currying),本质上是单参数函数的语法糖。
所谓柯里化,可以理解为把多参数的函数,转换成单参数的函数链,简单表示如下:
f(X1, X2, …, Xn)->->g(X1)(X2)…(Xn)
在前端日常开发中柯里化并不常用,但在面试中时常会被问到,个人猜测,最初问这个问题的人,也许是为了考查面试者对函数式编程的理解。
这里我们以一道经典的面试题入手:
请实现加法运算函数add()
,使其满足:
add(1, 2, 3); // 6
add(1, 2)(3); // 6
add(1)(2)(3); // 6
首先,要实现链式调用,add的返回值毫无疑问也是个函数;其次,因为这里连续调用的都是同样的加法运算,很容易想到调用自身的递归。
先来实现个简单版本的:
let sum = 0;
function add() {
sum += Array.from(arguments).reduce((x, y) => x + y);
add.toString = () => sum; // console.log时实际调用的是Function.toString()
return add;
}
这样写当然没毛病,但是在调用的时候,每次都需要手动将sum归零,否则sum就会被之前的计算污染。
我们很容易想到利用闭包来解决这个问题,实现纯函数版本的add()
:
function add() {
// 第一次调用时,就要对参数求和并保存到total中
let total = Array.from(arguments).reduce((x, y) => x + y);
// 利用闭包特性,保存total
const _add = function () { // 注意此处不能写成箭头函数
total += Array.from(arguments).reduce((x, y) => x + y);
return _add;
}
_add.toString = () => total;
return _add;
}
这里额外提一句,不要手里拿着锤子就到处找东西锤:
柯里化函数可用于任何支持闭包的编程语言;然而,出于效率原因,通常首选非柯里化函数,因为大多数函数调用可以避免部分应用程序和闭包创建的开销。
—— 维基百科
上面的 add()
函数还不够优雅,你看 reduce
重复调了两次,实际上我们可以把所有参数都先收集起来,最后调用一次就行:
function add() {
// 把参数保存到一个数组里
const arr = Array.from(arguments);
// 利用闭包特性,保存total
const _add = function () { // 注意此处不能写成箭头函数
arr.push(...Array.from(arguments));
return _add;
}
_add.toString = () => Array.from(arr).reduce((x, y) => x + y);
return _add;
}
让我们再抽象一层,把普通函数转换成柯里化函数的函数:
function fnToCurry(fn) {
const curry = function () {
// 第一次调用时,就把参数保存到数组里
const arr = Array.from(arguments);
// 利用闭包特性,保存total
const _fn = function () { // 注意此处不能写成箭头函数
arr.push(...Array.from(arguments));
return _fn;
}
_fn.toString = () => fn(...arr);
return _fn;
}
return curry; // 最后返回柯里化后的函数
}
用法示例:
function f1() {
return Array.from(arguments).reduce((x, y) => x + y);
}
// 将f1柯里化
const f2 = fnToCurry(f1);
console.log(f2(1, 2, 3)); // 6
console.log(f2(1)(2)(3)); // 6
上面这个柯里化后的函数会在调用时就立即执行,如果我们希望柯里化后的函数不立即执行,比如,只有当我们传入的参数包含 "exec"
的时候才执行,那么我们可以这样写:
function fnToDelayCurry(fn) {
const curry = function () {
const arr = Array.from(arguments)
// 利用闭包特性,保存total
const _fn = function () { // 注意此处不能写成箭头函数
// 当输入的参数中有exec标志时,立即执行
// 主要就是在这里去对传入的参数作判断,来判断是否执行
// 这里包含exec的参数不会被保存到arr中,若要保存exec,则把else中的push挪到外层即可
if (Array.from(arguments).indexOf("exec") > -1) {
_fn.toString = () => fn(...arr);
} else {
_fn.toString = Function.toString;
arr.push(...Array.from(arguments));
}
return _fn;
}
return _fn;
}
return curry;
}
用法示例:
function delayFn() {
const args = Array.from(arguments);
return args.join("~");
}
console.log(fnToDelayCurry(delayFn)(1)(2)(3)(4, 5)); // 这里打印的是函数_fn
console.log(fnToDelayCurry(delayFn)(1)(2)(3)(4, 5)("exec")); // 1~2~3~4~5
以上Demo代码都在这里:https://jsrun.net/ciUKp/edit
参考资料:
- 柯里化[wiki]:https://en.wikipedia.org/wiki/Currying
- 闭包:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
- λ演算-函数式语言的起源:https://zhuanlan.zhihu.com/p/164700404
往期文章
标签:function,谈些,return,函数,add,Currying,Array,我们,fn 来源: https://blog.csdn.net/qq_24734285/article/details/120902474