JavaScript中的记忆化介绍

55 阅读2分钟

简介

Memoization是许多编程语言中使用的一种优化技术,用于减少多余的、昂贵的函数调用的数量。这是通过缓存一个函数基于其输入的返回值来实现的。在这篇文章中,我们将创建一个次优的、但希望是具有教育意义的JavaScript函数记忆器。

首先,一个昂贵的函数需要记忆化

这里有一个函数供我们记忆。它以一种非常低效的方式找到一个数字的平方。

const inefficientSquare = (num) => {
  let total = 0;
  for (let i = 0; i < num; i++) {
    for (let j = 0; j < num; j++) {
      total++;
    }
  }
  return total;
};

我们可以用相同的值来运行这个函数,但每次都要花一些时间来执行。

const start = new Date();
inefficientSquare(40000);
console.log(new Date() - start);
// 1278

const start2 = new Date();
inefficientSquare(40000);
console.log(new Date() - start2);
// 1245

每次都要超过一秒钟,哎呀!

为我们的忆述器编写伪代码

在我们写任何代码之前,让我们来推理一下我们的备忘器。

  • 接受一个函数的引用作为输入
  • 返回一个函数(所以它可以像通常那样被使用)
  • 创建一个某种缓存,以保存以前的函数调用的结果
  • 以后再调用该函数时,如果缓存结果存在,则返回缓存结果
  • 如果缓存的值不存在,则调用该函数并将该结果存储在缓存中

真实的代码时间

下面是上面的伪代码大纲的实现。正如在介绍中提到的,这是次优的,你不应该在生产中使用这个。我将在后面解释为什么!

// Takes a reference to a function
const memoize = (func) => {
  // Creates a cache of results
  const results = {};
  // Returns a function
  return (...args) => {
    // Create a key for results cache
    const argsKey = JSON.stringify(args);
    // Only execute func if no cached value
    if (!results[argsKey]) {
      // Store function call result in cache
      results[argsKey] = func(...args);
    }
    // Return cached value
    return results[argsKey];
  };
};

这个实现中最不理想的部分,也是我不建议在生产代码中使用它的原因,是使用JSON.stringify ,在我们的results 缓存中创建键。JSON.stringify 的最大问题是它不能序列化某些输入,比如函数和符号(以及任何你在JSON中找不到的东西)。

在一个昂贵的函数上测试我们的Memoizer

让我们复制我们的inefficientSquare 例子,但这次我们将使用我们的备忘器来缓存结果。

const memoize = (func) => {
  const results = {};
  return (...args) => {
    const argsKey = JSON.stringify(args);
    if (!results[argsKey]) {
      results[argsKey] = func(...args);
    }
    return results[argsKey];
  };
};

const inefficientSquare = memoize((num) => {
  let total = 0;
  for (let i = 0; i < num; i++) {
    for (let j = 0; j < num; j++) {
      total++;
    }
  }
  return total;
});

const start = new Date();
inefficientSquare(40000);
console.log(new Date() - start);
// 1251

const start2 = new Date();
inefficientSquare(40000);
console.log(new Date() - start2);
// 0

成功了!第二次我们用同样的输入调用inefficientSquare ,它不需要重新计算的时间;我们只是从一个对象中提取缓存的值。

只对纯函数进行记忆化!

Memoization很好,但它只在你的函数是纯粹的情况下起作用。换句话说,如果你的函数的返回值不仅仅依赖于它的输入,那么你对这些输入的缓存值就不一定正确。另外,如果你的函数有副作用,记忆器不会复制这些,它只是返回最终返回的函数值。

结论

你现在应该对我们如何以及为什么使用记忆化有了一个很好的概念虽然我们的记忆化函数是次优的,但有很多第三方库你可以使用,会做得更好。只要确保你所记忆的函数是纯粹的就可以了。