闭包是实现函数记忆化最自然的方式——它通过外层函数创建私有缓存(如map),内层函数利用词法作用域持续访问该缓存,确保递归调用也命中缓存;推荐用map支持任意类型键,避免对象键误判,需规范键生成并防范内存泄漏。

闭包是实现函数记忆化的最自然方式——它让函数能私有地“记住”自己算过什么,下次遇到相同输入直接返回结果,跳过重复计算。关键不在加缓存,而在让每次调用(包括递归内部调用)都真正走缓存逻辑。
用闭包创建私有缓存空间
外层函数定义一个局部缓存容器(如 Map 或 对象),内层函数通过词法作用域持续访问它。这个缓存不会被外部干扰,也不会随外层函数执行结束而销毁。
- 推荐用 Map:支持任意类型键(基础类型可直接作键),无原型链污染,查找快
- 基础单参数场景可直接用参数作键:
cache.has(arg),无需序列化 - 避免用普通对象当缓存容器——
{}对undefined、NaN、原型属性等处理不一致
确保递归调用命中缓存
这是最容易出错的地方。如果原始函数内部仍调用自身函数名(比如 fibonacci(n-1)),那顶层套一层 memoize(fibonacci) 完全无效——子调用绕过了缓存。
- 正确做法:把缓存后的函数重新赋值给原名,例如
fibonacci = memoize((n) => {...}) - 或用 IIFE 封装整个逻辑,让递归体天然闭包引用缓存版本,避免命名污染
- 动手前加日志验证:
console.log('hit:', cache.has(key)),确认子调用也命中了
安全生成缓存键
键必须稳定、可比较、能准确区分不同输入组合。对象字面量不能直接当键({a:1} 和 {a:1} 是两个不同引用)。
- 多参数基础类型:用
args.join('|'),前提是不含|且顺序固定 - 含对象或不确定类型:写白名单键生成函数,例如只取
obj.id + '|' + obj.version - 慎用
JSON.stringify(args):会因undefined、函数、循环引用报错或误判
控制缓存生命周期,防内存泄漏
长期运行中,缓存可能积累大量键值对,占用内存并拖慢 GC。尤其递归深度大时更明显。
- 简单项目可用
cache.clear()手动重置 - 生产环境建议集成 LRU 策略(如
lru-cache库),自动淘汰旧项 - 警惕全局引用:若缓存函数绑定到事件监听器或挂到
window上,缓存将无法释放











