JavaScript 记忆
随着我们的系统成熟并开始进行更复杂的计算,对速度的需求也在增长,流程优化成为一种需求。当我们忽视这个问题时,我们最终会得到需要很长时间才能运行并需要大量系统资源的应用程序。
在本文中,我们将研究记忆化,这是一种可以帮助我们以正确方式显着减少处理时间的技术。
记忆化:记忆化是一种通过缓存昂贵的函数调用的结果并在再次使用相同的输入时返回它们来加速应用程序的技术。
让我们尝试通过将定义分解成小部分来理解这一点。
- 昂贵的函数调用:时间和内存是计算机应用程序中最重要的两个资源。因此,昂贵的函数调用是由于在执行期间的大量计算而消耗大量这两种资源的函数调用。
- 缓存:缓存只是一个临时数据存储,用于存储数据以便更快地为将来的数据请求提供服务。
记忆的重要性:当输入中给出一个函数时,它会执行必要的计算并将结果保存在缓存中,然后再返回值。如果以后再次收到相同的输入,则无需重复该过程。它只会从内存中返回缓存的答案。这将大大减少代码的执行时间。
Javascript 中的记忆:在 JavaScript 中,记忆的概念主要基于两个想法。它们如下:
- 闭包
- 高阶函数
闭包:在讨论闭包之前,让我们快速了解一下 JavaScript 中词法作用域的概念。词法作用域通过源代码中声明的变量的位置来定义变量的范围。
例子:
Javascript
let hello = "Hello";
function salutation() {
let name = "Aayush";
console.log(`${hello} ${name}!`);
}
Javascript
function salutation() {
let name = "Aayush";
function greet() {
console.log(`Hello ${name}!`);
}
greet();
}
Javascript
function salutation() {
let name = 'Aayush';
function greet() {
console.log(`Hello ${name}!`);
}
return greet;
}
let wish = salutation();
wish();
Javascript
function fibonacci(n) {
if (n < 2)
return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
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);
}
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
});
}
})
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});
}
})
在上面的代码中:
- 变量hello是一个全局变量。它可以从任何位置访问,包括salutation()函数。
- 变量名是一个局部变量,只能在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 结尾的数字,遵循每个数字(称为Fibonacci 数)等于其前两个数字之和的规则。
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的可选参数。我们使用缓存对象作为临时内存来存储斐波那契数及其相关索引作为键,然后可以在稍后的执行中根据需要检索。
如果我们绘制两个版本的 Fibonacci函数的执行时间,很明显,使用 memoization 技术可以显着减少时间。
实际示例: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 负载测试工具进行了快速负载测试。
Memoization 后的响应时间:现在让我们修改上面的代码以添加 memoization。出于此说明的目的,我使用了 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});
}
})
因此,
与上图相比,我们可以观察到使用 memoize 的 Express.js 路由明显快于非 memoize 等效路由。