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])
记忆递归函数
如果您尝试将递归函数传递到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