← 返回题目列表

如何手写 memoize 函数?缓存 key 应该怎么设计?

中等 第 19 / 27 题 更新于 2026/07/29
手写代码缓存memoize函数工具

简化版

memoize 用来缓存纯函数的计算结果。相同入参再次调用时直接返回缓存,避免重复计算。基础实现可以用 Map 存 key 和结果,key 可以由第一个参数、JSON 序列化或自定义 resolver 生成。边界包括:函数必须尽量纯、对象参数 key 不稳定、缓存可能无限增长、this 要保留、异步结果要考虑失败缓存。

详细版

memoize 的核心是“参数 → key → 结果”。

key 方案优点缺点
第一个参数简单只适合单参数
JSON.stringify(args)通用一点对函数、循环引用、属性顺序有坑
自定义 resolver最灵活调用者要设计
WeakMap 嵌套对对象友好实现复杂
function memoize<T extends (...args: any[]) => any>(fn: T, resolver?: (...args: Parameters<T>) => unknown): T {
  const cache = new Map<unknown, ReturnType<T>>()
  return function (this: unknown, ...args: Parameters<T>) {
    const key = resolver ? resolver(...args) : JSON.stringify(args)
    if (cache.has(key)) return cache.get(key)!
    const result = fn.apply(this, args)
    cache.set(key, result)
    return result
  } as T
}

memoize 只适合缓存“同样输入得到同样输出”的函数,不能随便包有副作用的函数。

完整版教学

一、memoize 解决什么问题

某些函数计算昂贵,例如递归、复杂格式化、规则匹配和数据转换。如果相同输入会反复出现,就可以缓存结果。

典型例子是斐波那契递归:

const fib = memoize((n: number): number => {
  if (n <= 1) return n
  return fib(n - 1) + fib(n - 2)
})

二、为什么强调纯函数

纯函数意味着相同输入总是得到相同输出,而且没有外部副作用。memoize 默认依赖这个前提。

如果函数读取当前时间、随机数、全局变量或接口状态,缓存结果可能错误。

三、基础实现

基础实现只需要 Map。

function simpleMemoize(fn: Function) {
  const cache = new Map()
  return (...args: unknown[]) => {
    const key = JSON.stringify(args)
    if (cache.has(key)) return cache.get(key)
    const value = fn(...args)
    cache.set(key, value)
    return value
  }
}

这足够展示思路,但不是工程最稳写法。

四、key 设计是核心

JSON.stringify 有很多坑:对象属性顺序可能影响字符串,函数和 undefined 会被特殊处理,循环引用会报错,大对象序列化本身也有成本。

因此成熟 memoize 通常允许传 resolver。

const getUser = memoize(fetchUser, userId => userId)

调用者最了解业务 key,强行通用反而容易错。

五、对象参数怎么办

对象参数如果按引用区分,可以使用 WeakMap。这样对象被释放时,缓存也不会强行阻止 GC。

参数类型推荐 key
基础类型直接作为 Map key
对象按引用WeakMap
对象按内容稳定序列化或业务 id

六、缓存淘汰问题

memoize 缓存如果无限增长,会变成内存泄漏。工程上可加:

  • 最大缓存数量。
  • TTL 过期。
  • LRU 淘汰。
  • 手动 clear。

手写题如果时间充足,可以主动提这点。

七、常见误区与追问

  • 误区:memoize 可以优化所有函数。 只有重复输入多、计算成本高、结果稳定时才有价值。
  • 误区:JSON.stringify 永远能当 key。 循环引用、函数、属性顺序和序列化成本都是坑。
  • 误区:缓存越多越好。 无限缓存可能造成内存增长,应该有淘汰策略。
  • 追问:异步函数能 memoize 吗? 可以缓存 Promise,但 reject 后是否删除缓存要按业务决定。
  • 追问:如何保留 this?fn.apply(this, args) 调用原函数。
  • 追问:对象参数用 Map 有什么问题? Map 会强引用对象,长期缓存可能阻止对象释放。

八、加强记忆

memoize 记成“纯函数结果缓存”。重点不是 Map,而是 key 设计、缓存边界和适用条件。面试回答只要把 resolver、this、淘汰和异步失败说清楚,就很完整。