手写 curry、compose 与 memoize,并说明实现边界。

先理解原理和示例,再练习完整口述与追问。

curry 累积参数并明确 arity;compose 从右往左传单值;memoize 使用参数元组和 this 索引;正确缓存 undefined、区分正负零;说明同步、占位符与容量边界。

以下实现针对普通可调用函数:curry 不支持占位符,以最后一次调用的 this 执行原函数,多余参数继续传入;默认参数与 rest 参数会影响 Function.length,因此可显式提供 arity。compose 只组合同步的一元步骤,空组合是恒等函数。memoize 用对象身份比较对象参数,连同 this 一起作键,仅用于适合缓存的同步函数;没有 TTL 或容量上限,生产长生命周期场景还需淘汰策略,也不承诺构造调用、异步重试或结构化深比较。

js
'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()),反转副本避免修改调用方数组。同步函数抛错时不会写入命中值,但目前建立的空索引节点仍会占空间;若经常接收不同失败输入,也应添加容量控制。返回对象是同一引用,调用方修改它会影响后续缓存结果,所以应约定不可变输出或按需求复制。

递归缓存需要内部也经过包装入口,例如:

js
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 或容量上限,生产长生命周期场景还需淘汰策略,也不承诺构造调用、异步重试或结构化深比较。