JavaScript实现一个记忆函数

122 阅读5分钟

说在前面

在编程的世界里,性能优化始终是一个重要的话题。今天,我们将一起来实现一个实用的记忆函数(简单来说,就是同样的入参,只会在第一次调用指定函数获取结果,后续则可以直接获取到第一次计算的结果返回),它能够显著提升函数调用的效率,特别是在处理重复计算的场景中。

需求

现给定一个函数 fn ,返回该函数的一个 记忆化 版本。

一个 记忆化 的函数是一个函数,它不会被相同的输入调用两次。而是会返回一个缓存的值。

函数 fn 可以是任何函数,对它所接受的值类型没有任何限制。如果两个输入值在 JavaScript 中使用 === 运算符比较时相等,则它们被视为相同。

示例1

  • 输入:
getInputs = () => [[2,2],[2,2],[1,2]]
fn = function (a, b) { return a + b; }
  • 输出:
[{"val":4,"calls":1},{"val":4,"calls":1},{"val":3,"calls":2}]
  • 解释:
const inputs = getInputs();
const memoized = memoize(fn);
for (const arr of inputs) {
  memoized(...arr);
}
对于参数为 (2, 2) 的输入: 2 + 2 = 4,需要调用 fn() 。
对于参数为 (2, 2) 的输入: 2 + 2 = 4,这些输入之前已经出现过,因此不需要再次调用 fn()。
对于参数为 (1, 2) 的输入: 1 + 2 = 3,需要再次调用 fn(),总共调用了 2 次。

示例2

  • 输入:
getInputs = () => [[{},{}],[{},{}],[{},{}]] 
fn = function (a, b) { return a + b; }
  • 输出:
  • 解释:
将两个空对象合并总是会得到一个空对象。尽管看起来应该缓存命中并只调用一次 fn(),但是这些空对象彼此之间都不是 === 相等的。

示例3

  • 输入:
getInputs = () => { const o = {}; return [[o,o],[o,o],[o,o]]; }
fn = function (a, b) { return ({...a, ...b}); }
  • 输出:
[{"val":{},"calls":1},{"val":{},"calls":1},{"val":{},"calls":1}]
  • 解释:
将两个空对象合并总是会得到一个空对象。因为传入的每个对象都是相同的,所以第二个和第三个函数调用都会命中缓存。

代码实现

1、入参处理

要避免重复计算相同入参,那我们就需要将每次计算的入参记录起来,但是入参会有多个,我们还需要将所有入参也记录起来,那么怎么将多个入参转成一个key呢?

  • 首先我们先简化一下,如果入参都是字符串的话我们会怎么进行处理?

没错,都是字符串的话我们可以直接使用连接符号将所有入参连接起来作为一个key。那么现在入参数据格式多种的情况我们应该要怎么处理呢?

  • 既然入参都是字符串的时候我们可以处理,那我们就给每一个入参赋予一个专属的id,如下图:

const idMap = new Map();
const getId = (k) => {
  if (idMap.has(k)) {
    return idMap.get(k);
  }
  const size = idMap.size;
  idMap.set(k, size);
  return size;
};

这个函数用于为传入的参数生成一个唯一的标识符。它首先检查 idMap 中是否已经存在该参数对应的标识符,如果存在,则直接返回;如果不存在,就将当前 idMap 的大小作为新的标识符,并将参数与标识符的映射关系存储到 idMap 中。

因为这里的入参可以是任意类型的数据,所以我们这里不能直接用Object对象来记录,可以使用Map来记录。

const a = {aa:1};
const b = {};
const c = new Map();
b[a] = 1;
c.set(a,1);
console.log(b);
console.log(c);
  • 这里我们得到的b为:
{
    "[object Object]": 1
}
  • c为:
new Map([
    [
        {
            "aa": 1
        },
        1
    ]
])

2、记录入参

遍历arguments,获取到每一个入参的专属id,最后使用-将所有id连接起来作为一组入参的key,将计算得到的值作为value保存。

const arr = [];
for (const element of arguments) {
  arr.push(getId(element));
}
const key = arr.join("-");
if (!valMap.has(key)) {
  valMap.set(key, fn(...arguments));
}
return valMap.get(key);

3、完整代码

/**
 * @param {Function} fn
 * @return {Function}
 */

function memoize(fn) {
  const idMap = new Map();
  const valMap = new Map();
  const getId = (k) => {
    if (idMap.has(k)) {
      return idMap.get(k);
    }
    const size = idMap.size;
    idMap.set(k, size);
    return size;
  };
  return function () {
    const arr = [];
    for (const element of arguments) {
      arr.push(getId(element));
    }
    const key = arr.join("-");
    if (!valMap.has(key)) {
      valMap.set(key, fn(...arguments));
    }
    return valMap.get(key);
  };
}

应用场景

  • 假设我们有一个复杂的对象,其中某个属性的计算需要消耗大量资源,并且该属性可能会被多次访问。
const complexObject = {
  data: {
    // 大量复杂数据
  },
  getComputedProperty: memoize(function () {
    // 复杂的计算逻辑,例如遍历 data 中的数据进行统计分析
    return result;
  })
};

console.log(complexObject.getComputedProperty());
console.log(complexObject.getComputedProperty());

在第一次访问 complexObject.getComputedProperty 时,会执行复杂的计算逻辑并缓存结果。后续再次访问时,直接返回缓存的结果,避免了重复计算,提高了整体性能。

公众号

关注公众号『前端也能这么有趣』,获取更多有趣内容。

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。