功能说明
memoize 把函数包装成带缓存的版本,默认用 JSON.stringify 序列化参数作为键,并为每个条目设置存活时间。命中未过期的条目时直接返回缓存值,并把该条目移到 Map 的末尾;过期条目会被删除并计入 expirations。缓存超过 maxSize 时淘汰最早插入的键,因此它同时具备简单的容量保护。
返回的包装函数附带 clear() 和 stats(),方便测试和运行时观测。now 可以注入自定义时钟,从而在测试中精确控制 TTL,而不必真的等待。所有参数校验都在包装时完成,避免在热路径上反复检查配置。
关键代码
命中逻辑先比较 expiresAt 与当前时间;未过期时删除再重新插入,以刷新 Map 的插入顺序。未命中则调用原函数,并在写入前检查容量。
const existing = cache.get(cacheKey);
if (existing !== undefined) {
if (existing.expiresAt > timestamp) {
stats.hits += 1;
cache.delete(cacheKey);
cache.set(cacheKey, existing);
return existing.value;
}
cache.delete(cacheKey);
stats.expirations += 1;
}
stats() 返回命中、未命中、淘汰、过期和当前大小的快照,不会暴露内部 Map。
使用注意
该实现缓存的是函数的返回值,不会自动等待 Promise。如果被包装函数是异步的,缓存中会保存 Promise,拒绝的 Promise 也会被缓存直到过期,调用方需要自行处理或在失败时清空。默认键依赖 JSON 序列化,undefined、函数、Symbol 以及属性顺序可能造成键冲突,生产环境建议传入稳定的自定义键函数。
TTL 从调用开始时计算,而不是从异步操作完成时计算;对耗时接近 TTL 的函数要留出余量。maxSize 淘汰的是最早插入的键,而不是最少使用的键,若需要 LRU 语义应改用专门实现。clear() 只清空缓存,不重置统计计数。若 key 或 now 抛错,异常会直接传播。
验证方式
示例对平方函数设置 40 毫秒 TTL:连续两次调用 slowSquare(4) 只执行一次,第三次调用不同参数后 calls 为 2,统计显示一次命中、两次未命中。等待 50 毫秒后再次调用,calls 变为 3,expirations 增加。调用 clear() 后 size 归零,可确认手动失效生效。