如何为具备交换律的输入实现可正确命中的Memoize缓存方案
实现顺序无关的等价参数缓存识别
你现有memoize函数的核心逻辑没有问题,预留的resolver参数本身就是用来自定义缓存键生成规则的,不需要改动memoize的主体代码,只要让等价的参数集合(元素完全一致、顺序不同)生成完全相同的缓存键,就能适配加法交换律的命中需求。
最简实现:利用resolver生成统一规则的缓存键
核心思路是:生成缓存键之前,先把传入的参数按固定规则排序,再拼接成无歧义的字符串,这样不管参数传入顺序是什么,只要元素内容一致,最终生成的键就完全相同。
// 针对满足交换律函数的缓存键生成器 const commutativeResolver = (...args) => { // 1. 先把每个参数转成字符串,避免类型混淆 // 2. 按固定规则排序(排序规则只要稳定统一即可,不需要特意做数值大小排序) // 3. 用特殊分隔符拼接,避免不同参数组合拼接后出现相同字符串的歧义问题 return args.map(String).sort().join('|') } // 传入resolver初始化记忆化函数 const memoizedAdd = memoize(add, commutativeResolver)
测试验证:
memoizedAdd(1,2,3) // 首次执行计算,结果写入缓存 memoizedAdd(2,1,3) // 键和上一次调用完全一致,直接读取缓存返回,不会重复执行计算 memoizedAdd(3,2,1) // 同样命中缓存
关键注意点:绝对不能直接用
args.sort().join('')生成键,否则会出现严重的键冲突:比如参数[1, 23]和[12, 3]直接拼接后都会得到字符串"123",导致两个完全不同的输入错误命中同一份缓存。拼接时加入不会出现在参数内容里的分隔符(比如|、,)就能彻底避免这个问题。
通用封装:支持所有顺序不影响结果的函数
如果你有多个满足交换律、结合律的函数(比如乘法、求最大值、集合求交集、纯属性对象合并等)需要做记忆化,可以直接封装成专门的顺序无关记忆化函数,不需要每次单独写resolver:
function memoizeCommutative(fn) { const cache = new Map() return function(...args) { // 对参数做序列化,支持引用类型参数(对象、数组等) // 统一排序后用分隔符拼接成无歧义的键 const key = args .map(arg => JSON.stringify(arg)) .sort() .join('|') if (cache.has(key)) return cache.get(key) const result = fn.apply(this, args) // 保留this指向,适配对象方法场景 cache.set(key, result) return result } }
这个通用版本的适配范围更广:
- 自动区分参数类型,不会把数字
1和字符串"1"识别成同一个参数 - 支持可序列化的引用类型参数,比如
memoizedAdd({a:1}, {b:2})和memoizedAdd({b:2}, {a:1})也能正确命中缓存 - 保留了原函数的this指向,用来记忆化对象方法时不会出现上下文错误
边界情况说明
- 如果参数里包含不可序列化的值(比如函数、Symbol、循环引用的对象),
JSON.stringify会出现转换异常,这类场景需要替换成对应适配的参数序列化规则。 - 如果参数存在顺序语义(比如减法、除法、字符串拼接),不要用这个方案,否则会出现错误的缓存命中。
内容的提问来源于stack exchange,提问作者Joji
相关产品推荐
相关产品推荐

