前端面试 P019:记忆化缓存与递归调用的纠错
来源:方格前端讲面试 · 19. 函数组合、记忆化,22 分 04 秒。
回看定位:09:42 缓存概念 · 17:31 递归缓存的原说法 · 19:15 缓存策略
记忆化的前提
根据输入缓存输出,重复输入时直接返回缓存。适合结果仅依赖明确输入的计算;缓存 IO、时间、随机数或依赖外部状态的函数,需要另外设计失效和一致性规则。
需要纠正:只包装外层不会自动缓存递归子问题
视频用 memoized = memoize(fibonacci) 后,声称第一次执行时会自动缓存所有内部 n。若原 fibonacci 内部仍调用原 fibonacci,这些递归调用根本没有经过包装器,缓存通常只记录最外层参数。
要缓存递归子问题,递归路径必须实际调用带缓存的入口,或把缓存直接写进递归函数:
function makeFibonacci() {
const cache = new Map([[0, 0], [1, 1]]);
function fibonacci(n) {
if (!Number.isInteger(n) || n < 0) throw new RangeError('Expected nonnegative integer');
if (cache.has(n)) return cache.get(n);
const result = fibonacci(n - 1) + fibonacci(n - 2);
cache.set(n, result);
return result;
}
return fibonacci;
}
const fibonacci = makeFibonacci();
console.log(fibonacci(10)); // 55
console.log(fibonacci(15)); // 610
对这个递推,缓存将重复计算压到与 n 相关的规模,但递归深度仍有限制,Number 结果也会遇到精度问题;不能把示例视作任意大整数算法。
key、this 与缓存大小
JSON.stringify 不是任意参数的无碰撞编码:undefined、函数、Symbol、NaN、循环引用、键顺序等都有问题。普通对象作缓存还会把键转成字符串,混淆不同输入。
Map 可按值或对象身份存键,但多参数要设计组合键;若函数结果依赖 this,也必须计入接收者或固定它。对象内容原地变化后,身份不变却可能让旧结果失效。
缓存还会延长参数和结果的存活时间,应根据场景考虑最大容量、LRU、TTL 或主动清理。缓存对象结果后,调用方修改它也会影响下一次命中结果。
Lodash.memoize 默认主要以第一个参数作为缓存键,并不是默认自动为所有参数做完美编码;多参数场景需要 resolver 或其他结构。