技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

JavaScript 专题之函数柯里化

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/28-JavaScript专题之函数柯里化
本文目录9 个章节
  1. 一、什么是函数柯里化
  2. 二、柯里化与部分应用不是一回事
  3. 三、一个可用的基础实现
  4. 四、Function.length 不是永远可靠的元数
  5. 五、占位符与参数顺序
  6. 六、柯里化异步函数
  7. 七、不要柯里化所有函数
  8. 八、测试示例
  9. 九、参考资料与原文

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,柯里化不能通过 callapplybind 改变它。需要绑定接收者时使用普通函数,并在调用约定中明确 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)

九、参考资料与原文

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS