如何使用Memoize来缓存JavaScript函数结果并加快代码的速度

Function 是编程的组成部分。它们有助于为我们的代码增加模块化和可重用性。

将程序划分为大块是很常见的功能,我们稍后可以使用这些功能来执行一些有用的操作。

有时,多次调用功能可能会变得昂贵(比如,计算一个数字的阶乘的函数)。但是有一种方法可以优化这些功能,使它们执行得更快:缓存

例如,假设我们function要返回一个数字的阶乘:

function factorial(n) {
    // Calculations: n * (n-1) * (n-2) * ... (2) * (1)
    return factorial
}

非常好,现在我们来看看factorial(50)。计算机会执行计算,并返回给我们最后的答案,不错!

完成后,让我们来看看factorial(51)。计算机再次执行一些计算并获得结果,但是您可能已经注意到我们已经重复了可以避免的一些步骤。优化的方式是:

factorial(51) = factorial(50) * 51;

但是我们function每次调用时都会从头开始执行计算:

factorial(51) = 51 * 50 * 49 * ... * 2 * 1

如果我们的函数factorial能记住它之前运算的结果并用它来提交运行效率,那不是很酷吗?

谈到Memoization,一种让我们的function记住(缓存)结果的方式。既然您对我们要实现的目标有一个基本的了解,这里是一个正式的定义:

Memoization是一种优化技术,主要用于通过 存储昂贵的函数调用的结果 来加速计算机程序,并在相同的输入再次发生时返回缓存的结果.

记住简单的东西意味着记忆或存储在内存中。记忆函数通常更快,因为如果随后使用先前的值调用该函数,我们将从缓存中获取结果来代替执行该函数,。

这看起来像一个简单的记忆函数(如果你想进行测试它,这里是一个CodePen):

// a simple function to add something
const add = (n) => (n + 10);
add(9);
// a simple memoized function to add something
const memoizedAdd = () => {
  let cache = {};
  return (n) => {
    if (n in cache) {
      console.log('Fetching from cache');
      return cache[n];
    }
    else {
      console.log('Calculating result');
      let result = n + 10;
      cache[n] = result;
      return result;
    }
  }
}
// returned function from memoizedAdd
const newAdd = memoizedAdd();
console.log(newAdd(9)); // calculated
console.log(newAdd(9)); // cached

Memoization 要点

以上代码的一些要点是:

memoizedAdd返回一个function以便稍后调用的。这是可能的,因为在JavaScript中,函数是一等公民,它们可以将它们用作高阶的函数并返回另一个函数。

cache可以记住它的值,因为返回的函数被闭包封装了。

memoized功能纯粹(Pure)是至关重要的。纯函数将为特定输入返回相同的输出,而不需要调用多少次,这使得cache按预期的工作。

编写自己的 memoize 功能

以前的代码工作正常,但如果我们想将任何函数转换为记忆函数呢?

以下是如何编写自己的memoize函数(codepen):

// a simple pure function to get a value adding 10
const add = (n) => (n + 10);
console.log('Simple call', add(3));
// a simple memoize function that takes in a function
// and returns a memoized function
const memoize = (fn) => {
  let cache = {};
  return (...args) => {
    let n = args[0];  // just taking one argument here
    if (n in cache) {
      console.log('Fetching from cache');
      return cache[n];
    }
    else {
      console.log('Calculating result');
      let result = fn(n);
      cache[n] = result;
      return result;
    }
  }
}
// creating a memoized function for the 'add' pure function
const memoizedAdd = memoize(add);
console.log(memoizedAdd(3));  // calculated
console.log(memoizedAdd(3));  // cached
console.log(memoizedAdd(4));  // calculated
console.log(memoizedAdd(4));  // cached

这很棒!这个简单的memoize功能将任何简单的function封装成一个有记忆性的等价物。该代码适用于简单的功能,可以根据您的需要轻松调整处理任意数量的arguments代码。另一个选择是利用一些的类库,如:

Lodash_.memoize(func, [resolver])

来自decko的 ES7 @memoize 装饰器

记忆递归函数

如果您尝试将递归函数传递到memoize上面的函数或Lodash的_.memoize函数,结果将不会如预期的那样,因为其后续调用的递归函数将最终调用自身而不是记忆函数,从而不再使用cache

只需确保递归函数调用记忆函数。这里是你如何调整教科书阶乘例子(codepen):

// same memoize function from before
const memoize = (fn) => {
  let cache = {};
  return (...args) => {
    let n = args[0];
    if (n in cache) {
      console.log('Fetching from cache', n);
      return cache[n];
    }
    else {
      console.log('Calculating result', n);
      let result = fn(n);
      cache[n] = result;
      return result;
    }
  }
}
const factorial = memoize(
  (x) => {
    if (x === 0) {
      return 1;
    }
    else {
      return x * factorial(x - 1);
    }
  }
);
console.log(factorial(5)); // calculated
console.log(factorial(6)); // calculated for 6 and cached for 5

从这段代码注意几点:

factorial函数递归地调用自己的记忆版本。

记忆功能是缓存先前阶乘的值,从而可以重复使用,从而显着改善了计算 factorial(6) = 6 * factorial(5)

memoization 是否与 caching 相同?

是的,有点。Memoization实际上是一种特定类型的缓存。虽然缓存通常可以引用任何存储技术(如HTTP缓存)以供将来使用,但是memoizing特指调用缓存的function返回值。

什么时候记录你的 functions

虽然可能看起来 memoize 可以与所有函数一起使用,但它实际上有限制的用例:

  • 为了记忆一个函数,它应该是纯粹的(pure),所以每次返回值对于相同的输入是相同的
  • 记忆是增加的空间和增加的速度之间的权衡,因此对于具有有限的输入范围的功能而言是重要的,以便可以更频繁地使用缓存的值
  • 它可能看起来像您记住您的API调用,但这并不是必需的,因为浏览器会为您自动缓存它们。有关详细信息,请参阅HTTP缓存
  • 我发现用于记忆功能的最佳用例是 重度计算功能,可以显着提高性能(阶乘和斐波那契不是真正现实世界的例子)
  • 如果您进入React / Redux,您可以检查重新选择哪个使用记忆选择器,以确保仅在状态树的相关部分发生更改时进行计算。

进一步阅读

如果您想更详细地了解本文中的一些主题,以下链接可能很有用:

【翻译原文】: https://medium.freecodecamp.org/understanding-memoize-in-javascript-51d07d19430e

0%