手写 curry、compose 与 memoize,并说明实现边界。
先理解原理和示例,再练习完整口述与追问。
回答要点
curry 累积参数并明确 arity;compose 从右往左传单值;memoize 使用参数元组和 this 索引;正确缓存 undefined、区分正负零;说明同步、占位符与容量边界。
原理拆解
以下实现针对普通可调用函数:curry 不支持占位符,以最后一次调用的 this 执行原函数,多余参数继续传入;默认参数与 rest 参数会影响 Function.length,因此可显式提供 arity。compose 只组合同步的一元步骤,空组合是恒等函数。memoize 用对象身份比较对象参数,连同 this 一起作键,仅用于适合缓存的同步函数;没有 TTL 或容量上限,生产长生命周期场景还需淘汰策略,也不承诺构造调用、异步重试或结构化深比较。
'use strict'
function curry(fn, arity) {
if (typeof fn !== 'function') throw new TypeError('fn 必须是函数')
if (arity === undefined) arity = fn.length
if (!Number.isInteger(arity) || arity < 0) throw new TypeError('arity 必须是非负整数')
function collect(previous) {
return function (...args) {
const all = previous.concat(args)
return all.length >= arity ? Reflect.apply(fn, this, all) : collect(all)
}
}
return collect([])
}
function compose(...functions) {
if (!functions.every(fn => typeof fn === 'function')) throw new TypeError('组合项必须是函数')
return function (value) {
return functions.reduceRight((result, fn) => Reflect.apply(fn, this, [result]), value)
}
}
function memoize(fn) {
if (typeof fn !== 'function') throw new TypeError('fn 必须是函数')
const negativeZero = Symbol('negativeZero')
const makeNode = () => ({ children: new Map(), hasValue: false, value: undefined })
const root = makeNode()
return function (...args) {
let node = root
for (const input of [this, ...args]) {
const key = Object.is(input, -0) ? negativeZero : input
if (!node.children.has(key)) node.children.set(key, makeNode())
node = node.children.get(key)
}
if (node.hasValue) return node.value
const value = Reflect.apply(fn, this, args)
node.value = value
node.hasValue = true
return value
}
}
例如 curry((a,b,c) => a+b+c)(1)(2,3) 得到 6;compose(x => x+1,x => x*2)(3) 得到 7。pipe 可以写成 compose(...functions.slice().reverse()),反转副本避免修改调用方数组。同步函数抛错时不会写入命中值,但目前建立的空索引节点仍会占空间;若经常接收不同失败输入,也应添加容量控制。返回对象是同一引用,调用方修改它会影响后续缓存结果,所以应约定不可变输出或按需求复制。
递归缓存需要内部也经过包装入口,例如:
let fastFib
fastFib = memoize(function (n) {
if (!Number.isInteger(n) || n < 0 || n > 78) throw new RangeError('仅演示 0~78 的安全整数结果')
return n < 2 ? n : fastFib(n - 1) + fastFib(n - 2)
})
这个递归例子在严格模式下以普通函数调用使用,保持相同的 this 缓存上下文。超过示例范围需改用 BigInt 并考虑递归栈;生产的数列计算也可以用迭代实现,避免仅为展示 memoize 就保留不必要的递归。
示例与推演
先测试 curry 的分组参数与最后一次调用 this,再测试 compose 方向和空组合;memoize 需覆盖多参数、对象身份、undefined 命中、异常不记值与 +0/-0。递归 Fibonacci 应确认内部调用的是缓存入口,才能复用子问题。正文实现的每个边界都与这些测试一一对应。
易错点
不要把同步一元 compose 当成异步洋葱模型。Map 缓存会强引用键;示例不含淘汰策略,长期生产使用应添加容量和失效。
参考回答可直接练习口述
以下实现针对普通可调用函数:curry 不支持占位符,以最后一次调用的 this 执行原函数,多余参数继续传入;默认参数与 rest 参数会影响 Function.length,因此可显式提供 arity。compose 只组合同步的一元步骤,空组合是恒等函数。memoize 用对象身份比较对象参数,连同 this 一起作键,仅用于适合缓存的同步函数;没有 TTL 或容量上限,生产长生命周期场景还需淘汰策略,也不承诺构造调用、异步重试或结构化深比较。