柯里化
面试题
实现一个 curry 函数,把 fn(a, b, c) 变成 curried(a)(b)(c) 或 curried(a, b)(c)
面试官视角
这题主要考闭包和参数收集:
- 如何保存已经传入的参数
- 什么时候执行原函数
- 是否支持一次传多个参数
this是否能正确透传- 是否知道
fn.length表示函数形参个数
什么是柯里化
柯里化是把一个多参数函数转换成一系列单参数或少参数函数
function add(a, b, c) {
return a + b + c
}
const curriedAdd = curry(add)
curriedAdd(1)(2)(3) // 6
curriedAdd(1, 2)(3) // 6
curriedAdd(1)(2, 3) // 6核心思路
- 获取原函数需要的参数个数
fn.length - 返回一个收集参数的函数
- 每次调用都把旧参数和新参数合并
- 如果参数数量达到要求,就执行原函数
- 如果参数数量还不够,就继续返回一个新函数收集剩余参数
基础实现
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
}
return function (...nextArgs) {
return curried.apply(this, [...args, ...nextArgs])
}
}
}更严谨的 this 版本
基础实现里,多次调用时 this 可能取决于最后一次调用。面试里可以说明:大多数柯里化场景不依赖 this;如果要固定第一次调用的 this,可以这样写
function curry(fn) {
function generate(prevArgs, context) {
return function (...nextArgs) {
const allArgs = [...prevArgs, ...nextArgs]
const finalContext = context || this
if (allArgs.length >= fn.length) {
return fn.apply(finalContext, allArgs)
}
return generate(allArgs, finalContext)
}
}
return generate([], null)
}支持占位符
如果面试官追问 Lodash 风格占位符,可以补这个思路,不一定要现场完整写
const _ = Symbol('placeholder')
function curry(fn, placeholder = _) {
function generate(prevArgs) {
return function (...nextArgs) {
const mergedArgs = []
let nextIndex = 0
for (const arg of prevArgs) {
if (arg === placeholder && nextIndex < nextArgs.length) {
mergedArgs.push(nextArgs[nextIndex++])
} else {
mergedArgs.push(arg)
}
}
mergedArgs.push(...nextArgs.slice(nextIndex))
const validArgsLength = mergedArgs
.slice(0, fn.length)
.filter((arg) => arg !== placeholder).length
if (validArgsLength >= fn.length) {
return fn(...mergedArgs.slice(0, fn.length))
}
return generate(mergedArgs)
}
}
return generate([])
}使用:
function join(a, b, c) {
return `${a}-${b}-${c}`
}
const curriedJoin = curry(join)
console.log(curriedJoin(_, 'b')('a', 'c')) // a-b-c应用场景
| 场景 | 说明 |
|---|---|
| 参数复用 | 固定一部分参数,生成专用函数 |
| 函数组合 | 更容易组合多个单参数函数 |
| 延迟执行 | 参数没收集齐前不执行 |
| 事件处理 | 预置业务参数,再等待事件对象 |
示例:
function request(method, url, data) {
return fetch(url, {
method,
body: JSON.stringify(data)
})
}
const post = curry(request)('POST')
post('/api/user')({ name: 'byte' })面试回答模板
柯里化的核心是闭包保存参数。每次调用都把当前参数合并到之前收集的参数里,如果数量达到原函数的形参数量,就用这些参数执行原函数;如果还不够,就继续返回一个函数等待下一次传参。实现上可以通过 fn.length 判断需要几个参数
易错点
- 要支持一次传多个参数,不只是
curried(a)(b)(c) fn.length不包含有默认值参数之后的参数- 如果原函数依赖
this,要考虑this透传策略 - 柯里化和偏函数类似,但柯里化强调逐步收集直到参数够了再执行
