首頁  >  文章  >  web前端  >  深入了解JavaScript 快取

深入了解JavaScript 快取

青灯夜游
青灯夜游轉載
2020-11-06 17:55:022852瀏覽

深入了解JavaScript 快取

隨著我們的應用程式的不斷增長並開始進行複雜的計算時,對速度的需求越來越高,所以流程的最佳化變得必不可少。當我們忽略這個問題時,我們最終的程序需要花費大量時間並在執行期間消耗大量的系統資源。

推薦教學:《JavaScript影片教學

快取是一種最佳化技術,透過儲存開銷大的函數執行的結果,並在相同的輸入再次出現時傳回已快取的結果,從而加快應用程式的速度。

如果這對你沒有多大意義,那也沒關係。本文深入解釋了為什麼需要進行緩存,緩存是什麼,如何實現以及何時應該使用緩存。

什麼是快取

快取是一種最佳化技術,透過儲存開銷大的函數執行的結果,並在相同的輸入再次出現時傳回已快取的結果,從而加快應用程式的速度。

在這一點上,我們很清楚,快取的目的是減少執行「昂貴的函數呼叫」所花費的時間和資源。

什麼是昂貴的函數呼叫?別搞混了,我們不是在這裡花錢。在電腦程式的上下文中,我們擁有的兩個主要資源是時間和記憶體。因此,一個昂貴的函數呼叫是指一個函數呼叫中,由於計算量大,在執行過程中大量佔用了電腦的資源和時間。

然而,就像對待金錢一樣,我們需要節約。為此,使用快取來儲存函數呼叫的結果,以便在將來的時間內快速方便地存取。

快取只是一個臨時的數據存儲,它保存數據,以便將來對該數據的請求能夠更快地處理。

因此,當一個昂貴的函數被呼叫一次時,結果被儲存在快取中,這樣,每當在應用程式中再次呼叫該函數時,結果就會從快取中非常快速地取出,而不需要重新進行任何計算。

為什麼快取很重要?

下面是一個實例,說明了快取的重要性:

想像一下,你正在公園裡讀一本封面很吸引人的新小說。每次一個人經過,他們都會被封面吸引,所以他們會問書名和作者。第一次被問到這個問題的時候,你翻開書,唸出書名和作者的名字。現在越來越多的人來這裡問同樣的問題。你是一個很好的人,所以你回答所有問題。

你會翻開封面,把書名和作者的名字一一告訴他,還是開始憑記憶回答?哪個能節省你更多的時間?

發現其中的相似之處了嗎?使用記憶法,當函數提供輸入時,它執行所需的計算並在返回值之前將結果儲存到快取中。如果將來接收到相同的輸入,它就不必一遍又一遍地重複,它只需要從快取(記憶體)中提供答案。

快取是怎麼運作的

JavaScript 中的快取的概念主要建立在兩個概念之上,它們分別是:

  • 閉包
  • 高階函數(傳回函數的函數)

#閉包

##閉包是函數和宣告該函數的詞法環境的組合。
不是很清楚? 我也這麼認為。

為了更好的理解,讓我們快速研究一下 JavaScript 中詞法作用域的概念,詞法作用域只是指程式設計師在編寫程式碼時指定的變數和區塊的物理位置。如下程式碼:

function foo(a) {
  var b = a + 2;
  function bar(c) {
    console.log(a, b, c);
  }
  bar(b * 2);
}

foo(3); // 3, 5, 10
從這段程式碼中,我們可以確定三個作用域:

    全域作用域(包含
  • foo 作為唯一識別碼)
  • foo 作用域,它有標識符abbar
  • ## bar
  • 作用域,包含c 標識符
  • 仔細查看上面的程式碼,我們注意到函數
foo

可以存取變數a 和b,因為它嵌套在foo 中。請注意,我們成功地儲存了函數 bar 及其運行環境。因此,我們說 barfoo 的作用域上有一個閉包。 你可以在遺傳的背景下理解這一點,即個體有機會獲得並表現出遺傳特徵,即使是在他們當前的環境之外,這個邏輯突出了閉包的另一個因素,引出了我們的第二個主要概念。

從函數傳回函數

透過接受其他函數作為參數或傳回其他函數的函數稱為高階函數。

閉包允許我們在封閉函數的外部呼叫內部函數,同時保持對封閉函數的詞法作用域的存取

讓我們對前面的範例中的程式碼進行一些調整,以解釋這一點。

function foo(){
  var a = 2;

  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz();//2

注意函数 foo 如何返回另一个函数 bar。这里我们执行函数 foo 并将返回值赋给baz。但是在本例中,我们有一个返回函数,因此,baz 现在持有对 foo 中定义的bar 函数的引用。

最有趣的是,当我们在 foo 的词法作用域之外执行函数 baz 时,仍然会得到 a 的值,这怎么可能呢?

请记住,由于闭包的存在,bar 总是可以访问 foo 中的变量(继承的特性),即使它是在 foo 的作用域之外执行的。

案例研究:斐波那契数列

斐波那契数列是什么?

斐波那契数列是一组数字,以1 或 0 开头,后面跟着1,然后根据每个数字等于前两个数字之和规则进行。如

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

或者

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

挑战:编写一个函数返回斐波那契数列中的 n 元素,其中的序列是:

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …]

知道每个值都是前两个值的和,这个问题的递归解是:

function fibonacci(n) {
  if (n <p>确实简洁准确!但是,有一个问题。请注意,当 <code>n</code> 的值到终止递归之前,需要做大量的工作和时间,因为序列中存在对某些值的重复求值。</p><p>看看下面的图表,当我们试图计算 <code>fib(5)</code>时,我们注意到我们反复地尝试在不同分支的下标 <code>0,1,2,3</code> 处找到 Fibonacci 数,这就是所谓的冗余计算,而这正是缓存所要消除的。</p><p><img src="https://img.php.cn/upload/image/136/162/211/1604656285576117.jpg" title="1604656285576117.jpg" alt="深入了解JavaScript 快取"></p><pre class="brush:php;toolbar:false">function fibonacci(n, memo) {
  memo = memo || {}
  if (memo[n]) {
    return memo[n]
  }
  if (n <p>在上面的代码片段中,我们调整函数以接受一个可选参数 <code>memo</code>。我们使用 <code>memo</code> 对象作为缓存来存储斐波那契数列,并将其各自的索引作为键,以便在执行过程中稍后需要时检索它们。</p><pre class="brush:php;toolbar:false">memo = memo || {}

在这里,检查是否在调用函数时将 memo 作为参数接收。如果有,则初始化它以供使用;如果没有,则将其设置为空对象。

if (memo[n]) {
  return memo[n]
}

接下来,检查当前键 n 是否有缓存值,如果有,则返回其值。

和之前的解一样,我们指定了 n 小于等于 1 时的终止递归。

最后,我们递归地调用n值较小的函数,同时将缓存值(memo)传递给每个函数,以便在计算期间使用。这确保了在以前计算并缓存值时,我们不会第二次执行如此昂贵的计算。我们只是从 memo 中取回值。

注意,我们在返回缓存之前将最终结果添加到缓存中。

使用 JSPerf 测试性能

可以使用些链接来性能测试。在那里,我们运行一个测试来评估使用这两种方法执行fibonacci(20) 所需的时间。结果如下:

深入了解JavaScript 快取

哇! ! !这让人很惊讶,使用缓存的 fibonacci 函数是最快的。然而,这一数字相当惊人。它执行 126,762 ops/sec,这远远大于执行 1,751 ops/sec 的纯递归解决方案,并且比较没有缓存的递归速度大约快 99%。

注:“ops/sec”表示每秒的操作次数,就是一秒钟内预计要执行的测试次数。

现在我们已经看到了缓存在函数级别上对应用程序的性能有多大的影响。这是否意味着对于应用程序中的每个昂贵函数,我们都必须创建一个修改后的变量来维护内部缓存?

不,回想一下,我们通过从函数返回函数来了解到,即使在外部执行它们,它们也会导致它们继承父函数的范围,这使得可以将某些特征和属性从封闭函数传递到返回的函数。

使用函数的方式

在下面的代码片段中,我们创建了一个高阶的函数 memoizer。有了这个函数,将能够轻松地将缓存应用到任何函数。

function memoizer(fun) {
  let cache = {}
  return function (n) {
    if (cache[n] != undefined) {
      return cache[n]
    } else {
      let result = fun(n)
      cache[n] = result
      return result
    }
  }
}

上面,我们简单地创建一个名为 memoizer 的新函数,它接受将函数 fun 作为参数进行缓存。在函数中,我们创建一个缓存对象来存储函数执行的结果,以便将来使用。

memoizer 函数中,我们返回一个新函数,根据上面讨论的闭包原则,这个函数无论在哪里执行都可以访问 cache

在返回的函数中,我们使用 if..else 语句检查是否已经有指定键(参数) n 的缓存值。如果有,则取出并返回它。如果没有,我们使用函数来计算结果,以便缓存。然后,我们使用适当的键 n 将结果添加到缓存中,以便以后可以从那里访问它。最后,我们返回了计算结果。

很顺利!

要将 memoizer 函数应用于最初递归的 fibonacci 函数,我们调用 memoizer 函数,将 fibonacci 函数作为参数传递进去。

const fibonacciMemoFunction = memoizer(fibonacciRecursive)

测试 memoizer 函数

当我们将 memoizer 函数与上面的例子进行比较时,结果如下:

深入了解JavaScript 快取

memoizer 函数以 42,982,762 ops/sec 的速度提供了最快的解决方案,比之前考虑的解决方案速度要快 100%。

关于缓存,我们已经说明什么是缓存 、为什么要有缓存和如何实现缓存。现在我们来看看什么时候使用缓存。

何时使用缓存

当然,使用缓存效率是级高的,你现在可能想要缓存所有的函数,这可能会变得非常无益。以下几种情况下,适合使用缓存:

  • 对于昂贵的函数调用,执行复杂计算的函数。
  • 对于具有有限且高度重复输入范围的函数。
  • 用于具有重复输入值的递归函数。
  • 对于纯函数,即每次使用特定输入调用时返回相同输出的函数。

缓存库

总结

使用缓存方法 ,我们可以防止函数调用函数来反复计算相同的结果,现在是你把这些知识付诸实践的时候了。

更多编程相关知识,请访问:编程入门!!

以上是深入了解JavaScript 快取的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除