深入理解JavaScript柯里化:从入门到实践
前言
最近在学习面试题时,经常看到"柯里化"这个词。说实话,第一次看到 add(1)(2) 这种写法时,我也是一脸懵:为什么函数要这样调用?这样写有什么好处?
通过一段时间的学习,我终于搞懂了柯里化的原理和应用。这篇文章会按照我的学习路径,用最简单的方式,带你一步步理解柯里化。
第一章:从一个最简单的函数开始
1.1 我们最熟悉的函数调用
先来看一个最基础的加法函数:
javascript
// 1.js
function add(a, b) {
return a + b
}
console.log(add(1, 2)) // 3
这是我们最熟悉的函数调用方式------一次性传入所有参数。这种方式简单直观,但有没有想过:如果我想先传入第一个参数,过一会儿再传入第二个参数,该怎么办?
1.2 尝试分步传入参数
JavaScript给了我们一种可能性------函数可以作为返回值:
javascript
// 2.js
function add(a) {
return function(b) {
return a + b
}
}
console.log(add(1)(2)) // 3
- 打印结果
看到区别了吗?原来的 add(1, 2) 变成了 add(1)(2)。
这就是手动柯里化的雏形------将一个接收多个参数的函数,转换成一系列接收单个参数的函数。
1.3 理解这个过程的本质
让我们拆解一下 add(1)(2) 的执行过程:
javascript
// 第一步:调用 add(1)
const fn = add(1) // fn 是一个函数:function(b) { return 1 + b }
// 第二步:调用 fn(2)
const result = fn(2) // 3
// 合并起来就是 add(1)(2)
- 打印结果

关键点 :这里利用了闭包 的特性------内部函数可以访问外部函数的变量 a。即使外部函数执行完毕,变量 a 也不会被销毁,因为它被内部函数引用了。
第二章:手动柯里化的局限性
2.1 如果函数需要三个参数呢?
javascript
// 两个参数的版本
function add2(a) {
return function(b) {
return a + b
}
}
// 三个参数的版本
function add3(a) {
return function(b) {
return function(c) {
return a + b + c
}
}
}
console.log(add3(1)(2)(3)) // 6
发现问题了吗?每增加一个参数,就要多嵌套一层函数。
2.2 如果有多个函数需要柯里化
javascript
function add(a, b, c) { return a + b + c }
function multiply(a, b, c) { return a * b * c }
function concat(a, b, c) { return '' + a + b + c }
// 每个函数都要手动改写成嵌套函数,太麻烦了!
function addCurried(a) {
return function(b) {
return function(c) {
return a + b + c
}
}
}
// ... 还要为 multiply、concat 每个函数都写一遍,这样大麻烦了,所以我们来设计一种新的模式
第三章:通用柯里化函数的实现
3.1 设计思路
我们需要一个"柯里化工厂",它能:
- 接收一个普通函数作为参数
- 返回一个可以收集参数的函数
- 当参数收集够了,才执行原函数
3.2 第一步:获取函数需要的参数个数
javascript
// 3.js
function add(a, b) {
return a + b
}
// 函数的 length 属性表示它期望的参数个数
console.log(add.length) // 2
每个函数都有一个 length 属性,返回函数定义的参数个数。这正好可以用来判断参数是否收集够了。
- 打印结果

3.3 第二步:实现柯里化函数
javascript
function add(a, b) {
return a + b
}
function curry(fn) {
// 返回一个函数,用于收集参数
return function curried(...args) {
// 如果收集的参数数量 >= 原函数需要的参数数量
if (args.length >= fn.length) {
// 参数够了,直接执行原函数
return fn(...args)
}
// 参数还不够,返回一个新函数继续收集
return (...rest) => curried(...args, ...rest)
}
}
const curriedAdd = curry(add)
console.log(curriedAdd(1)(2)) // 3
- 打印结果

3.4 理解执行过程
让我们一步步追踪 curriedAdd(1)(2) 的执行:
javascript
// 第一次调用 curriedAdd(1)
// args = [1]
// args.length(1) < fn.length(2),返回新函数
// 返回 (...rest) => curried([1], ...rest)
// 第二次调用 (2)
// rest = [2]
// 调用 curried([1], [2]) => curried(1, 2)
// 此时 args = [1, 2]
// args.length(2) >= fn.length(2)
// 执行 add(1, 2) 返回 3
核心机制:
- 闭包:每次调用都通过闭包保存已收集的参数
- 递归:参数不够就返回新函数,继续收集
- 退出条件:参数数量达到原函数的要求
第四章:为什么要用 ...rest?
4.1 一个常见的疑问
有同学可能会问:既然柯里化是一个参数一个参数地传,为什么实现时要写成 (...rest) => curried(...args, ...rest),而不是 (arg) => curried(...args, arg)?
4.2 两种实现的对比
javascript
// 版本A:使用 ...rest(我们的实现)
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return (...rest) => curried(...args, ...rest) // 可以接收多个参数
}
}
// 版本B:只接收单个参数
function curry2(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return (arg) => curried(...args, arg) // 只能接收一个参数
}
}
const curriedAdd = curry(add)
const curriedAdd2 = curry2(add)
// 测试不同的调用方式
console.log(curriedAdd(1)(2)) // ✅ 3
console.log(curriedAdd(1, 2)) // ✅ 3(版本A支持)
console.log(curriedAdd2(1)(2)) // ✅ 3
console.log(curriedAdd2(1, 2)) // ❌ 第二个参数丢失(版本B不支持)
4.3 为什么需要支持多个参数?
考虑一个更复杂的函数:
javascript
function greet(greeting, name, punctuation) {
return `${greeting}, ${name}${punctuation}`
}
const curriedGreet = curry(greet)
// 实际开发中,我们可能需要各种调用方式
curriedGreet('Hello')('World')('!') // 分三次传
curriedGreet('Hello', 'World')('!') // 前两个一起传
curriedGreet('Hello')('World', '!') // 后两个一起传
curriedGreet('Hello', 'World', '!') // 一次性传完
// 使用 ...rest 的实现支持以上所有方式
// 使用固定参数的实现只支持第一种方式
结论 :...rest 的存在让柯里化函数更灵活,能够适应各种调用场景。
第五章:柯里化的实际应用
理论说完了,来看看柯里化在实际开发中能做什么。
5.1 固定函数参数(最常用)
javascript
// 4.js
// 日志函数
const log = type => message => {
console.log(`${type}: ${message}`)
}
// 固定日志类型,创建专用的日志函数
const errorLog = log('error')
const infoLog = log('info')
const warnLog = log('warn')
// 使用时只需关注消息内容
errorLog('数据库连接失败') // error: 数据库连接失败
infoLog('用户登录成功') // info: 用户登录成功
warnLog('磁盘空间不足') // warn: 磁盘空间不足
- 打印结果
这样做的好处:
- 提升语义 :
errorLog比log('error')更容易理解 - 减少重复:不用每次都传 'error'
- 便于维护 :如果日志格式要改,只需修改
log函数
5.2 延迟计算(大家可以复制代码自己运行输出一下)
javascript
const add = curry((a, b, c) => a + b + c)
// 这些调用都没有真正计算
const add1 = add(1) // 还在收集参数
const add1And2 = add1(2) // 还在收集参数
// 直到参数齐了才计算
const result = add1And2(3) // 6,真正执行加法
5.3 参数复用(大家可以复制代码自己运行输出一下)
javascript
// 处理数组的函数
const map = curry((fn, arr) => arr.map(fn))
const filter = curry((fn, arr) => arr.filter(fn))
// 复用转换逻辑
const double = map(x => x * 2) // 创建一个"加倍"函数
const even = filter(x => x % 2 === 0) // 创建一个"取偶数"函数
const numbers = [1, 2, 3, 4, 5]
// 应用到不同的数组
console.log(double(numbers)) // [2, 4, 6, 8, 10]
console.log(even(numbers)) // [2, 4]
const moreNumbers = [10, 20, 30]
console.log(double(moreNumbers)) // [20, 40, 60] 复用 double 逻辑
5.4 与函数组合配合(大家可以复制代码自己运行输出一下)
javascript
// 函数组合:从右往左执行
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x)
// 创建一些柯里化函数
const addOne = curry((x, y) => x + y)(1)
const double = x => x * 2
const square = x => x * x
// 组合成新函数:先加1,再乘2,再平方
const addOneThenDoubleThenSquare = compose(square, double, addOne)
console.log(addOneThenDoubleThenSquare(3))
// 3 -> 加1得4 -> 乘2得8 -> 平方得64
第六章:总结与思考
6.1 柯里化的核心要点
通过本文的学习,我们掌握了:
- 什么是柯里化:将多参数函数转换成一系列单参数函数的嵌套调用
- 柯里化的本质:利用闭包收集参数,当参数数量满足条件时执行函数
- 柯里化的实现:闭包 + 递归,配合 fn.length 判断参数是否足够
- 灵活性的设计:使用 ...rest 支持多种参数传递方式
- 实际应用场景:固定参数、延迟计算、参数复用、函数组合等
6.2 柯里化 vs 普通函数调用
| 对比维度 | 普通函数 | 柯里化函数 |
|---|---|---|
| 调用方式 | fn(a, b, c) |
fn(a)(b)(c) 或 fn(a, b)(c) |
| 参数传递 | 一次性 | 可分步 |
| 适用场景 | 简单调用 | 参数复用、延迟计算 |
| 学习成本 | 低 | 中 |
最后
柯里化不仅仅是一个面试题,更是一种实用的编程思想。它让我们的代码更加灵活、可复用,也是函数式编程中的重要概念。
希望这篇文章能帮助正在学习柯里化的你。如果有什么理解不到位的地方,欢迎在评论区交流讨论!