JavaScript 记忆化
随着系统的成熟和越来越复杂的计算,对速度的需求增长,过程优化成为一个需求。当我们忽视这个问题时,最终会导致应用程序执行时间长,需要大量系统资源。
在本文中,我们将学习一种名为记忆化的技术,如果使用得当可以显著减少处理时间。
记忆化: 记忆化是一种通过缓存昂贵函数调用的结果并在再次使用相同输入时返回它们来加速应用程序的技术。
我们来分解下这个定义来理解它的意思。
- 昂贵函数调用: 时间和内存是计算机应用程序中最重要的资源。因此,昂贵函数调用是指由于执行期间的广泛计算而消耗大量这两种资源的函数调用。
- 缓存: 缓存只是一个临时数据存储器,用于更快地响应将来对该数据的请求。
记忆化的重要性: 当一个函数作为输入给定时,它会执行必要的计算并在返回结果之前将结果保存在缓存中。如果将来再次收到相同的输入,不需要重复该过程。它将从内存中返回缓存的答案。这将大大减少代码的执行时间。
JavaScript中的记忆化: 在JavaScript中,记忆化的概念主要基于两个思想。它们如下:
-
闭包
-
高阶函数
闭包: 在谈论闭包之前,让我们快速了解一下JavaScript中的词法作用域的概念。词法作用域通过变量在源代码中声明的位置来定义变量的作用域。
示例:
Javascript
let hello = "Hello";
function salutation() {
let name = "Aayush";
console.log(`{hello}{name}!`);
}
在上面的代码中:
- 变量 hello 是一个全局变量。它可以从任何位置访问,包括 salutation() 函数。
- 变量 name 是一个局部变量,只能在 salutation() 函数内部访问。
根据词法作用域的规则,作用域可以嵌套,内部函数可以访问外部作用域中声明的变量。因此,在下面的代码中,内部函数 greet() 可以访问变量 name 。
Javascript
function salutation() {
let name = "Aayush";
function greet() {
console.log(`Hello ${name}!`);
}
greet();
}
现在让我们修改这个 salutation() 函数,而不是调用 greet() 函数,我们返回 greet() 函数对象。
Javascript
function salutation() {
let name = 'Aayush';
function greet() {
console.log(`Hello ${name}!`);
}
return greet;
}
let wish = salutation();
wish();
如果我们运行这段代码,我们将得到与之前相同的输出。然而,值得注意的是,局部变量通常只在函数执行期间存在。这意味着当 salutation() 执行完成后,变量 name 就不再可访问。在这种情况下,当我们执行 wish() 时,对于 greet() 的引用,变量 name 仍然存在。闭包是一个在其内部作用域中保留了外部作用域的函数。 高阶函数: 高阶函数是对其他函数进行操作的函数,它们接受函数作为参数或返回函数。例如,在上面的代码中,salutation() 是一个高阶函数。 现在,使用著名的斐波那契数列,让我们看看记忆化如何利用这些概念。 斐波那契数列: 斐波那契数列是一系列以1开头以1结尾的数,按照每个数字(称为斐波那契数)等于其前两个数字之和的规则生成。
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...
解决这个问题的一个简单的递归方法是:
Javascript
function fibonacci(n) {
if (n < 2)
return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
如果我们要绘制上述函数的递归树,当 n=4 时,它会像这样:

正如您可能注意到的,存在太多冗余计算。
让我们尝试使用备忘录来修复这个问题。
Javascript
function memoisedFibonacci(n, cache) {
cache = cache || [1, 1]
if (cache[n])
return cache[n]
return cache[n] = memoisedFibonacci(n - 1, cache) +
memoisedFibonacci(n - 2, cache);
}
将上面的代码示例中的函数更改为接受一个可选参数,名为 cache 。我们使用 cache 对象作为一个临时存储Fibonacci数与它们关联索引的内存,这些数可以在后面的执行中根据需要检索。

如果我们绘制斐波那契函数的两个版本的执行时间,很明显使用记忆化技术可以显著减少时间。
实际示例:为Web响应使用JavaScript记忆化: 我们将使用一个示例Idioms API来演示,它是一个使用Node.js构建的简单REST API。
记忆化之前的响应时间: 下面是一个简单的Express.js路由,返回API中存储的所有习语。在这种情况下,每次调用都会导致数据库查询。
JavaScript
import express from 'express';
const router = express.Router();
import { getAllIdioms } from '../services/database.js';
router.get('/', async function(req, res, next) {
try {
res.json(await getAllIdioms());
} catch (err) {
console.log('Error while getting idioms ', err.message);
res.status(err.statusCode || 500).json({
'message': err.message
});
}
})
让我们看一下这种方法的响应时间。我使用了 Vegeta 负载测试工具进行了快速负载测试。

在记忆化后的响应时间: 现在让我们修改上面的代码来添加记忆化。为了说明目的,我使用了 p-memoize 包。
Javascript
import express from 'express';
const router = express.Router();
import { getAllIdioms } from '../services/database.js';
import pMemoize from 'p-memoize';
const ONE_MINUTE_IN_MS = 60000;
const memGetAllIdioms = pMemoize(getAllIdioms, { maxAge: ONE_MINUTE_IN_MS });
router.get('/', async function (req, res, next) {
try {
res.json(await memGetAllIdioms());
} catch (err) {
console.log('Error while getting idioms ', err.message);
res.status(err.statusCode || 500).json({'message': err.message});
}
})
结果,

与之前的图形相比,我们可以观察到使用记忆化的Express.js路由比非记忆化的对应项要快得多。
极客教程