闭包是构建函数记忆化系统的天然选择——它让缓存数据与计算逻辑绑定在同一作用域内,既私有又持久,不依赖全局变量,也不暴露内部状态。

核心结构:两层函数 + 私有 Map 缓存
外层函数负责初始化一个 Map 实例,作为专属缓存容器;内层函数通过闭包持续访问该 Map,每次调用时先查键、再决定是否执行原函数。这种结构保证:
- 每个
memoize(fn)调用生成独立缓存,多个函数互不影响 - Map 支持任意类型键(数字、字符串、Symbol、甚至函数),比普通对象更安全可靠
- 缓存不会被垃圾回收,因为始终被返回的函数引用
多参数与健壮键生成
单参数基础类型可直接用参数作键;但实际中多数函数含多个参数或对象,需归一化为唯一、可比较的键:
- 固定数量基础类型:用
${a}|${b}|${c}拼接,高效且可控 - 动态参数列表:可用
JSON.stringify([a, b, ...args]),但要避开undefined、function和循环引用 - 生产环境推荐白名单序列化:例如只取对象的
id和version字段拼接成键,避免误判
必须防范的三大风险
加了缓存不等于性能提升,几个常见疏漏反而拖慢系统:
-
内存泄漏:缓存无上限增长,尤其键来自时间戳、随机 ID 或用户输入。建议暴露
.clear()方法,或集成 LRU 策略 -
键失效:
{x: 1}和{x: 1}是两个不同对象,Map 默认用===判断,永远不命中。必须标准化后再入键 -
异步陷阱:若缓存的是 Promise,后续调用拿到的是同一实例——适合防重复请求,但无法主动刷新;如需更新,得配合
Promise.race()或重载机制
递归函数的记忆化要点
斐波那契等自调用函数最容易踩坑:
- 错误做法:缓存后仍让内部递归调用原始函数名(如
fibonacci(n-1)),导致缓存完全失效 - 正确做法:将缓存后的函数重新赋值给原名(如
fib = memoize(fib)),或用 IIFE 封装,确保递归体天然引用缓存版本 - 务必验证命中:加日志
console.log('cache hit:', cache.has(key)),确认子调用也走缓存路径











