JavaScript函数柯里化

本文最后更新于 2025年7月30日 下午

参考:https://juejin.cn/post/6844903882208837645

什么是柯里化

在数学和计算机科学中,柯里化是种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

举例来说,一个接收 3 个参数的普通函数,在进行柯里化后,柯里化版本的函数接收一个参数并返回接收下一个参数的函数,该函数返回一个接收第三个参数的函数。最后一个函数在接收第三个参数后, 将之前接收到的三个参数应用于原普通函数中,并返回最终结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 数学和计算科学中的柯里化

// 一个接收三个参数的普通函数
function sum(a, b, c) {
console.log(a + b + c);
}

// 用于将普通函数转化为柯里化版本的工具函数
function curry(fn) {
// 内部实现省略,返回一个新函数
}

// 获取一个柯里化后的函数
let _sum = curry(sum);

let A = _sum(1); // 返回一个接收第二个参数的函数
let B = A(2); // 返回一个接收第三个参数的函数
B(3); // 接收到最后一个参数,将之前所有的参数应用到原函数中,并运行得到 6

对于 Javascript 语言,我们通常说的柯里化函数的概念与数学和计算机科学中的柯里化的概念不完全一样。

在数学和计算机科学中的柯里化函数一次只能传递一个参数,
而 Javascript 实际应用中的柯里化函数,可以传递一个或多个参数。

1
2
3
4
5
6
7
8
9
10
11
// 普通函数
function fn(a, b, c, d, e) {
console.log(a, b, c, d, e);
}
// 正常生成的柯里化函数
let _fn = curry(fn);

_fn(1, 2, 3, 4, 5); // 1 2 3 4 5
_fn(1)(2)(3, 4, 5); // 1 2 3 4 5
_fn(1, 2)(3, 4)(5); // 1 2 3 4 5
_fn(1)(2)(3)(4)(5); // 1 2 3 4 5

柯里化的用途

柯里化实际是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度。其中对于函数参数的自由处理,正是柯里化的核心所在。柯里化本质上是降低通用性,提高适用性。

假定我们有这样一段数据:

1
const list = [{ name: "lucy" }, { name: "jack" }];

我们需要获取数据中的所有 name 属性的值,常规思路下,我们会这样实现:

1
const names = list.map((item) => item.name);

用柯里化的思维来实现:

1
2
3
4
let prop = curry(function (key, obj) {
return obj[key];
});
const names = list.map(prop("name"));

仅仅只是为了获取 name 的属性值,为何还要实现一个 prop 函数呢,这样太麻烦了吧。

我们可以换个思路,prop 函数实现一次后,以后是可以多次使用的,所以我们在考虑代码复杂程度的时候,是可以将 prop 函数的实现去掉的。我们实际的代码可以理解为只有一行:

1
const names = list.map(prop("name"));

这么看来,通过柯里化的方式,我们的代码变得更精简并且可读性更高。

封装柯里化工具函数

对于柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?有两种思路:

  • 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数
  • 在调用柯里化工具函数时,手动指定所需的参数个数

两点结合以下,实现一个简单 curry 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 将函数柯里化
* @param fn 待柯里化的原函数
* @param len 所需的参数个数,默认为原函数的形参个数
*/
function curry(fn, len = fn.length) {
return _curry.call(this, fn, len);
}
/**
* 中转函数
* @param fn 待柯里化的原函数
* @param len 所需的参数个数
* @param args 已接收的参数列表
*/
function _curry(fn, len, ...args) {
return function (...params) {
let _args = [...args, ...params];
if (_args.length >= len) return fn.apply(this, _args);
else return _curry.call(this, fn, len, ..._args);
};
}

验证:

1
2
3
4
5
6
7
8
let _fn = curry(function (a, b, c, d, e) {
console.log(a, b, c, d, e);
});

_fn(1, 2, 3, 4, 5); // 1 2 3 4 5
_fn(1)(2)(3, 4, 5); // 1 2 3 4 5
_fn(1, 2)(3, 4)(5); // 1 2 3 4 5
_fn(1)(2)(3)(4)(5); // 1 2 3 4 5

而且,可以通过占位符的方式来改变传入参数的顺序。比如说,我们传入一个占位符,本次调用传递的参数略过占位符,占位符所在的位置由下次调用的参数来填充。

使用占位符的目的是改变参数传递的顺序,所以在 curry 函数实现中,每次需要记录是否使用占位符,并且记录占位符所代表的参数位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* @param fn 待柯里化的函数
* @param length 需要的参数个数,默认为函数的形参个数
* @param holder 占位符,默认当前柯里化函数
* @return {Function} 柯里化后的函数
*/
function curry(fn, length = fn.length, holder = curry) {
return _curry.call(this, fn, length, holder, [], []);
}
/**
* 中转函数
* @param fn 柯里化的原函数
* @param length 原函数需要的参数个数
* @param holder 接收的占位符
* @param args 已接收的参数列表
* @param holders 已接收的占位符位置列表
* @return {Function} 继续柯里化的函数 或 最终结果
*/
function _curry(fn, length, holder, args, holders) {
return function (..._args) {
//将参数复制一份,避免多次操作同一函数导致参数混乱
let params = args.slice();
//将占位符位置列表复制一份,新增加的占位符增加至此
let _holders = holders.slice();
//循环入参,追加参数 或 替换占位符
_args.forEach((arg, i) => {
//真实参数 之前存在占位符 将占位符替换为真实参数
if (arg !== holder && holders.length) {
let index = holders.shift();
_holders.splice(_holders.indexOf(index), 1);
params[index] = arg;
}
//真实参数 之前不存在占位符 将参数追加到参数列表中
else if (arg !== holder && !holders.length) {
params.push(arg);
}
//传入的是占位符,之前不存在占位符 记录占位符的位置
else if (arg === holder && !holders.length) {
params.push(arg);
_holders.push(params.length - 1);
}
//传入的是占位符,之前存在占位符 删除原占位符位置
else if (arg === holder && holders.length) {
holders.shift();
}
});
// params 中前 length 条记录中不包含占位符,执行函数
if (
params.length >= length &&
params.slice(0, length).every((i) => i !== holder)
) {
return fn.apply(this, params);
} else {
return _curry.call(this, fn, length, holder, params, _holders);
}
};
}

验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
let fn = function (a, b, c, d, e) {
console.log([a, b, c, d, e]);
};

let _ = {}; // 定义占位符
let _fn = curry(fn, 5, _); // 将函数柯里化,指定所需的参数个数,指定所需的占位符

_fn(1, 2, 3, 4, 5); // [1, 2, 3, 4, 5]
_fn(_, 2, 3, 4, 5)(1); // [1, 2, 3, 4, 5]
_fn(1, _, 3, 4, 5)(2); // [1, 2, 3, 4, 5]
_fn(1, _, 3)(_, 4, _)(2)(5); // [1, 2, 3, 4, 5]
_fn(1, _, _, 4)(_, 3)(2)(5); // [1, 2, 3, 4, 5]
_fn(_, 2)(_, _, 4)(1)(3)(5); // [1, 2, 3, 4, 5]

JavaScript函数柯里化
https://xuekeven.github.io/2021/10/12/函数柯里化/
作者
Keven
发布于
2021年10月12日
更新于
2025年7月30日
许可协议