先记住这个答案
柯里化是把 add(a, b, c) 变成 add(a)(b)(c) 这种分次传参的形式。实现靠闭包:每调用一次,就返回一个新函数,新函数通过闭包记住之前所有的参数。每次调用时把新旧参数合在一起,数量达到原函数需要的个数就执行原函数,否则继续返回新函数等着收参数。原函数需要几个参数,一般用 fn.length 判断。
- 每次调用返回新函数,闭包攒着前面的参数
- 参数数量够了才真正执行原函数
- 默认参数和剩余参数会让
fn.length变小 - 用处是提前固定部分参数,得到更专用的函数
手写一个支持分批传参的 curry
面试里最常要求的是这种:既能 add(1)(2)(3),也能 add(1, 2)(3)。思路就是一个递归的收集函数。
function curry(fn, arity = fn.length) {
return function collect(...args) {
if (args.length >= arity) return fn(...args) // 参数够了,执行原函数
return (...next) => collect(...args, ...next) // 不够,把旧参数存进闭包
}
}
const add = curry((a, b, c) => a + b + c)
console.log(add(1)(2)(3))
console.log(add(1, 2)(3))
const add10 = add(10) // 固定第一个参数
console.log(add10(1)(2), add10(5, 5)) // 两次调用互不影响查看输出与解释
6
6
13 20每次返回的箭头函数,都通过闭包拿着当时的 args。add(10) 返回的函数里 args 是 [10],之后不管调用多少次,都是在 [10] 的基础上再拼新参数,生成新数组,原来那份不会被改。所以 add10 可以反复复用。
为什么要留一个 arity 参数
fn.length 并不总是函数真正需要的参数个数,这是手写 curry 最容易出问题的地方。
fn.length什么时候不准length只统计第一个默认参数之前的形参,剩余参数不算。比如(a, b = 1, c) => {}的length是 1,(...nums) => {}的length是 0。拿这种函数去 curry,第一次调用就会直接执行,或者根本停不下来。所以通用实现要允许调用方手动传arity,比如curry(sum, 3)。- 参数多传了怎么办
- 上面的实现是参数达到或超过
arity就执行,多出来的一起传给原函数,原函数自己决定用不用。有些库会截断到正好arity个,这属于约定,面试时说明你选了哪种即可。
柯里化在项目里有什么用
核心价值是提前固定一部分参数,得到一个更专用的函数。比如 const logError = log('error'),之后到处调用 logError(msg),不用每次都传级别。再比如数组处理里 list.map(multiply(2)),比写 list.map((x) => multiply(2, x)) 更短。
- 什么时候别用
- 业务代码里只固定一两个参数的话,直接写一个普通函数,或者用
fn.bind(null, 'error'),更容易读。通用的 curry 会让调用栈变深,TypeScript 类型推导也很难写准确。函数式风格的库,比如 Ramda、lodash/fp,才会大量使用柯里化。
容易答错的地方
- 把柯里化和偏函数当成一回事
- 偏函数是一次固定几个参数,返回一个接收剩余参数的函数,
bind就是偏函数。柯里化是把函数改造成可以一个一个传参的链。两者都靠闭包,上面的 curry 实现同时支持两种用法,但概念上要分清。 - 用一个共享数组存参数
- 有人在 curry 外面定义一个
let args = [],每次调用往里push。这样第一次用完,数组里还残留着旧参数,第二次调用结果就错了,add10这种复用也做不到。必须每次调用都生成新数组。
面试官还会怎么问?
怎么实现 add(1)(2)(3)() 这种不定参数的版本?
不看参数个数,改成看是否传了空调用。每次有参数就继续收集,遇到 () 空调用时才把攒下的参数求和返回。另一种写法是让返回的函数重写 toString 或 valueOf,在被当作数字使用时输出结果,但这种写法依赖隐式转换,可读性差。
柯里化后 this 还能用吗?
上面的实现没有处理 this,调用原函数时 this 是 undefined。如果要包装对象方法,需要在 collect 里用普通函数记下 this,最后用 fn.apply(this, args) 调用。一般建议只对不依赖 this 的纯函数做柯里化。
柯里化和函数组合有什么关系?
compose(f, g)(x) 要求每个函数都只接收一个参数。柯里化能把多参数函数变成一次接收一个参数的形式,所以两者经常一起用,先用 curry 固定配置参数,再把得到的单参数函数串起来。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。