JavaScript 专题之函数柯里化
Category(分类): JavaScript Status: 已整理(2026)
原文只有标题和来源链接。本文保留原文来源,并补回函数柯里化的概念、实现、边界和现代 JavaScript 用法。
一、什么是函数柯里化
函数柯里化(currying)是把一个接收多个参数的函数,转换成一系列每次接收部分参数的函数。以三元函数为例:
function add(a, b, c) {
return a + b + c
}
add(1, 2, 3)
// 柯里化后的调用形式
addCurried(1)(2)(3)
这里的重点不是“必须每次只传一个参数”,而是把“收集参数”和“最终执行”分成多个调用步骤。JavaScript 社区实现通常允许一次传入多个参数:
addCurried(1, 2)(3)
addCurried(1)(2, 3)
addCurried(1, 2, 3)
ECMAScript 没有内置的 curry() 函数;柯里化是函数式编程中的一种组合技巧。Function.prototype.bind() 是标准 API,但它主要用于预绑定 this 和部分参数,不能自动提供所有柯里化语义。
二、柯里化与部分应用不是一回事
两者很接近,但关注点不同:
- 部分应用(partial application):提前固定部分参数,得到一个参数更少的新函数;
- 柯里化(currying):把一个多参数函数转换为连续收集参数的函数链,直到达到约定的元数(arity)才执行。
function multiply(a, b, c) {
return a * b * c
}
// 部分应用:预先固定第一个参数
const double = multiply.bind(null, 2)
console.log(double(3, 4)) // 24
// bind 还会绑定 this,因此它不是通用 curry 的完整替代品
在工程代码中,部分应用往往已经足够。不要为了使用“柯里化”这个术语,把清晰的普通函数调用改成难以调试的多层闭包。
三、一个可用的基础实现
下面的实现支持:
- 一次传入一个或多个参数;
- 通过显式
arity指定何时执行; - 参数数量达到
arity后把多余参数继续传给原函数; - 保留第一次调用时的
this,但不改变箭头函数本来就具有的词法this。
function curry(func, arity = func.length) {
if (typeof func !== 'function') {
throw new TypeError('func 必须是函数')
}
if (!Number.isInteger(arity) || arity < 0) {
throw new RangeError('arity 必须是非负整数')
}
function collect(collected, receiver, hasReceiver) {
return function curriedStep() {
'use strict'
const nextArgs = Array.from(arguments)
const allArgs = collected.concat(nextArgs)
const nextReceiver = hasReceiver ? receiver : this
if (allArgs.length >= arity) {
return Reflect.apply(func, nextReceiver, allArgs)
}
return collect(allArgs, nextReceiver, true)
}
}
return collect([], undefined, false)
}
function add(a, b, c) {
return a + b + c
}
const curriedAdd = curry(add)
console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
console.log(curriedAdd(1)(2, 3)) // 6
console.log(curriedAdd(1, 2, 3, 4)) // 6:多余参数仍传给 add,但 add 不使用
这里使用 Reflect.apply() 而不是把参数拼成字符串执行 eval(),可以正确传递任意 JavaScript 值,也不会引入动态代码执行。
this 的边界
函数柯里化会创建多层函数,this 的语义必须明确。上面的实现把第一次调用步骤的 this 作为最终调用的接收者:
const calculator = {
base: 10,
add(a, b) {
return this.base + a + b
}
}
calculator.curriedAdd = curry(calculator.add)
console.log(calculator.curriedAdd(1)(2)) // 13
如果不需要动态 this,更推荐使用参数显式传递依赖:
const addWithBase = (base, a, b) => base + a + b
const curriedAddWithBase = curry(addWithBase)
console.log(curriedAddWithBase(10)(1)(2)) // 13
箭头函数没有自己的 this,柯里化不能通过 call、apply 或 bind 改变它。需要绑定接收者时使用普通函数,并在调用约定中明确 this。
四、Function.length 不是永远可靠的元数
省略 arity 时,上例使用 func.length。但 Function.length 只反映函数定义中第一个默认参数之前的形参数量,并且不包含 rest 参数:
function normal(a, b, c) {}
function withDefault(a, b = 1, c) {}
function withRest(a, ...rest) {}
console.log(normal.length) // 3
console.log(withDefault.length) // 1
console.log(withRest.length) // 1
因此,带默认参数、rest 参数、重载式参数约定或需要忽略某些参数时,应显式传入 arity:
function format(prefix, value, suffix = '') {
return `${prefix}${value}${suffix}`
}
const curriedFormat = curry(format, 2)
console.log(curriedFormat('value: ')(10)) // value: 10
如果原函数本身是 (...args) => ...,它的 length 通常是 0。这时无条件使用 func.length 会使柯里化函数过早执行,必须提供业务约定的元数。
五、占位符与参数顺序
基础实现只会从左到右收集参数,不支持“先填第二个参数、稍后再填第一个参数”的占位符语义:
// 基础实现不支持这种写法
// curriedAdd(_, 2)(1, 3)
占位符需要额外定义唯一标记、参数填充顺序、重复占位符和多余参数规则。Lodash 等库可能提供自己的占位符约定,使用前应阅读具体版本文档,不要把不同库的行为混在一起。
六、柯里化异步函数
柯里化本身不改变返回值类型。如果原函数返回 Promise,最终一步得到的仍然是 Promise:
async function loadUser(baseUrl, id) {
const response = await fetch(`${baseUrl}/users/${id}`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
}
const loadFromApi = curry(loadUser, 2)('https://example.com/api')
async function demo() {
const user = await loadFromApi(42)
console.log(user)
}
demo()
上面的顶层 await 需要模块脚本或支持顶层 await 的运行环境。柯里化不会自动处理取消、重试、并发和错误边界;网络请求仍应单独设计这些问题。
七、不要柯里化所有函数
以下情况通常不值得引入柯里化:
- 参数数量本身不固定,或者由用户输入决定;
- 函数副作用很重,调用链难以观察;
- 团队不熟悉该约定,调试成本高于复用收益;
- 需要构造
class实例。类构造器不能像普通函数一样通过Reflect.apply()调用,应使用new或工厂函数; - 框架 API 已经提供清晰的配置对象或组合式 API。
柯里化更适合参数顺序稳定、函数较纯、需要复用前置配置的场景,例如格式化器、校验器、查询构造器和日志标签函数。
八、测试示例
const assert = require('node:assert/strict')
const sum = (a, b, c) => a + b + c
const curriedSum = curry(sum)
assert.equal(curriedSum(1)(2)(3), 6)
assert.equal(curriedSum(1, 2)(3), 6)
assert.equal(curriedSum(1)(2, 3), 6)
assert.equal(curry(sum, 2)(1)(2, 3), 3)