关键在于控制行为边界、避免重复计算、确保调用关系清晰;需用闭包使递归调用天然走缓存,键要安全稳定(如简单参数直接作key、多参数用join('|')、对象取白名单字段),配合lru等策略管控生命周期,并通过日志验证各级调用是否真实命中缓存。

写可维护的递归逻辑,关键不在“让它跑起来”,而在于控制它的行为边界、避免重复计算、并让调用关系清晰可读。闭包和缓存不是锦上添花的技巧,而是解决递归固有缺陷(如指数爆炸、栈深不可控、重复子问题)的底层支撑。
让递归真正走缓存,而不是只套一层壳
很多开发者给递归函数包一层 memoize,却发现性能没改善——根本原因是递归体内部仍调用原始函数名,缓存被完全绕过。
- 错误写法:定义 fib(n),再写 const memoFib = memoize(fib),但 fib 内部还是调 fib(n-1) → 子调用不经过缓存
- 正确做法:用闭包封装整个逻辑,让递归调用天然指向缓存版本
例如:const fib = (function() { const cache = new Map(); return function f(n) { if (n - 或直接重赋值:fib = memoize((n) => n —— 注意这里右侧用的是新绑定的 fib
缓存键必须安全且稳定
缓存失效往往不是因为没存,而是因为键对不上。JSON.stringify(args) 看似方便,但在真实场景中极易出错。
- 基础类型单参数(如数字、字符串):直接用 Map,key 就是参数本身
- 多参数且全为简单类型:用 args.join('|'),前提是确保参数不含 | 字符
- 含对象、数组或不确定类型:写白名单键生成器,例如只取 obj.id + '|' + obj.version,忽略其他字段
- 绝对不要把对象字面量当 key:{a:1} !== {a:1},Map 默认按引用比较
控制缓存生命周期,避免内存失控
递归深度大时,缓存可能积累数千项。长期驻留不仅吃内存,还会拖慢 GC,尤其在长时运行服务中。
- 简单场景:手动加 cache.clear() 或设最大容量后定期清理
- 生产环境推荐 LRU 策略,比如集成 lru-cache 库,注入到闭包作用域中统一管理
- 警惕全局绑定:如果缓存函数被挂到 window、事件监听器或模块顶层导出,它就很难被回收
- 浮点数作 key 要格外小心:0.1 + 0.2 !== 0.3,可能导致缓存未命中
验证缓存是否真生效,别靠猜
加一行日志比读十遍文档更管用。缓存是否起效,不能看顶层调用快了没,要看子调用有没有跳过计算。
- 在缓存读取逻辑里加:console.log('hit:', cache.has(key))
- 跑两次相同输入,确认第二次所有中间调用都命中缓存,而非仅顶层命中
- 纯函数是前提:若函数依赖外部状态(如修改全局变量、读 DOM、发请求),缓存结果就不可信











