在 JavaScript 中,柯里化(Currying) 是一种函数式编程技术,它将一个接受多个参数的函数转换为一系列只接受单个参数的函数。简单来说,就是把 f(a, b, c)
变成 f(a)(b)(c)
,逐步传递参数,直到所有参数齐全后执行原始函数。让我们从入门到精通,逐步理解柯里化的概念、实现和应用。
一、入门:什么是柯里化?
基本概念
- 定义:柯里化是将一个多参数函数分解为多个单参数函数的过程。
- 原始形式:
f(a, b, c)
。 - 柯里化后:
f(a)(b)(c)
。 - 核心思想:延迟执行,逐步收集参数,直到参数足够时计算结果。
简单例子
假设有一个加法函数:
1 2 3 4 |
function add(a, b, c) { return a + b + c; } console.log(add(1, 2, 3)); // 6 |
柯里化后:
1 2 3 4 5 6 7 8 |
function curriedAdd(a) { return function(b) { return function(c) { return a + b + c; }; }; } console.log(curriedAdd(1)(2)(3)); // 6 |
- 变化:从一次性传入三个参数,变成分三次传入,每次一个。
为什么叫“柯里化”?
- 名字来源于数学家 Haskell Curry,这种技术在函数式编程语言(如 Haskell)中很常见。
入门理解
- 类比:像流水线工人,每个人只负责一部分工作(一个参数),最后组装成完整产品(结果)。
- 好处:提高函数的复用性和灵活性。
二、初级:手动实现柯里化
手动柯里化
对于固定参数的函数,可以手动嵌套函数实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function multiply(a, b, c) { return a * b * c; } // 手动柯里化 function curriedMultiply(a) { return function(b) { return function(c) { return a * b * c; }; }; } console.log(curriedMultiply(2)(3)(4)); // 24 |
- 过程:
curriedMultiply(2)
返回一个函数,记住a = 2
。- 这个函数再接收
b = 3
,返回另一个函数。 - 最后接收
c = 4
,计算2 * 3 * 4 = 24
。
局限性
- 只适用于固定参数数量的函数。
- 如果参数数量变化,手动写嵌套会很麻烦。
三、中级:通用柯里化函数
动态实现
我们需要一个通用的 curry
函数,能处理任意参数数量的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(null, args); } return function(...moreArgs) { return curried.apply(null, args.concat(moreArgs)); }; }; } function multiply(a, b, c) { return a * b * c; } const curriedMultiply = curry(multiply); console.log(curriedMultiply(2)(3)(4)); // 24 console.log(curriedMultiply(2, 3)(4)); // 24 console.log(curriedMultiply(2, 3, 4)); // 24 |
- 关键点:
fn.length
:目标函数的形参数量(这里是3
)。args
:当前收集的参数。- 如果
args.length >= fn.length
,执行fn
。 - 否则,返回新函数继续收集参数。
执行流程(以 (2)(3)(4)
为例)
curriedMultiply(2)
:
args = [2]
,1 < 3
。- 返回新函数,记住
args = [2]
。
(3)
:
moreArgs = [3]
,合并为[2, 3]
,2 < 3
。- 返回新函数,记住
[2, 3]
。
(4)
:
moreArgs = [4]
,合并为[2, 3, 4]
,3 >= 3
。- 执行
multiply(2, 3, 4)
,返回24
。
中级理解
- 动态性:支持任意参数数量。
- 递归:通过函数闭包和递归收集参数。
四、高级:优化与灵活性
1. 支持占位符
有时我们想跳过某些参数,用占位符(如 _
)表示稍后填充:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function curry(fn) { const placeholder = Symbol('placeholder'); return function curried(...args) { if (args.length >= fn.length && !args.includes(placeholder)) { return fn.apply(null, args); } return function(...moreArgs) { const merged = []; let mi = 0; for (let i = 0; i < args.length && mi < moreArgs.length; i++) { merged[i] = args[i] === placeholder ? moreArgs[mi++] : args[i]; } for (; mi < moreArgs.length; mi++) merged.push(moreArgs[mi]); return curried.apply(null, merged); }; }; } const _ = Symbol('placeholder'); const add = curry((a, b, c) => a + b + c); console.log(add(1, _, 3)(2)); // 6(1 + 2 + 3) |
- 改进:用
placeholder
允许部分参数延迟传入。
2. 使用箭头函数简化
现代 JavaScript 可以用箭头函数简化实现:
1 2 3 4 5 6 7 8 |
const curry = fn => (...args) => args.length >= fn.length ? fn(...args) : (...moreArgs) => curry(fn)(...args, ...moreArgs); const multiply = (a, b, c) => a * b * c; const curried = curry(multiply); console.log(curried(2)(3)(4)); // 24 |
- 简洁性:用
...
代替apply
和concat
。
五、精通:应用场景与实战
1. 函数复用
- 场景:创建特定功能的专用函数。
- 示例:
1 2 3 4 5 6 7 8 |
const curry = fn => (...args) => args.length >= fn.length ? fn(...args) : (...more) => curry(fn)(...args, ...more); const add = (a, b) => a + b; const curriedAdd = curry(add); const increment = curriedAdd(1); // 固定第一个参数为 1 console.log(increment(5)); // 6 console.log(increment(10)); // 11 |
- 好处:
increment
复用了add
,固定了增量。
2. 管道化操作
- 场景:结合柯里化实现函数组合。
- 示例:
1 2 3 4 5 6 7 |
const curry = fn => (...args) => args.length >= fn.length ? fn(...args) : (...more) => curry(fn)(...args, ...more); const map = curry((fn, arr) => arr.map(fn)); const double = x => x * 2; const doubleAll = map(double); console.log(doubleAll([1, 2, 3])); // [2, 4, 6] |
3. 事件处理
- 场景:动态绑定参数。
- 示例:
1 2 3 4 5 6 |
const curry = fn => (...args) => args.length >= fn.length ? fn(...args) : (...more) => curry(fn)(...args, ...more); const log = curry((prefix, message) => console.log(`${prefix}: ${message}`)); const errorLog = log("ERROR"); errorLog("Something went wrong"); // "ERROR: Something went wrong" |
4. 与高阶组件结合
- 在 React 中,柯里化常用于配置高阶组件:
1 2 3 4 5 6 7 |
const withProps = curry((props, Component) => { return function Wrapped(props2) { return <Component {...props} {...props2} />; }; }); const Enhanced = withProps({ color: "red" })(({ color }) => <div>{color}</div>); |
六、注意事项与精通要点
- 性能:
- 柯里化会创建多个函数闭包,可能增加内存开销。
- 适合逻辑复用场景,不建议滥用。
- 与偏函数(Partial Application)区别:
- 柯里化:每次只传一个参数。
- 偏函数:一次性固定部分参数。
- 示例:
123const partialAdd = (a, b, c) => a + b + c;const add5 = partialAdd.bind(null, 5); // 偏函数console.log(add5(2, 3)); // 10
- 调试:
- 柯里化函数是嵌套结构,错误栈可能较深,调试时注意参数传递。
总结:从入门到精通
- 入门:理解柯里化是将
f(a, b)
转为f(a)(b)
。 - 初级:手动实现固定参数的柯里化。
- 中级:用递归和
apply
实现通用柯里化。 - 高级:增加占位符、用箭头函数优化。
- 精通:掌握应用场景(如复用、管道、事件处理)。
类比理解
- 像点菜:普通函数是一次点齐所有菜(
add(1, 2, 3)
),柯里化是分次点菜(add(1)(2)(3)
),最后上桌(计算结果)。