摘要:專題系列第十七篇,講解函數記憶與菲波那切數列的實現定義函數記憶是指將上次的計算結果緩存起來,當下次調用時,如果遇到相同的參數,就直接返回緩存中的數據。
定義JavaScript 專題系列第十七篇,講解函數記憶與菲波那切數列的實現
函數記憶是指將上次的計算結果緩存起來,當下次調用時,如果遇到相同的參數,就直接返回緩存中的數據。
舉個例子:
function add(a, b) { return a + b; } // 假設 memorize 可以實現函數記憶 var memoizedAdd = memorize(add); memoizedAdd(1, 2) // 3 memoizedAdd(1, 2) // 相同的參數,第二次調用時,從緩存中取出數據,而非重新計算一次原理
實現這樣一個 memorize 函數很簡單,原理上只用把參數和對應的結果數據存到一個對象中,調用時,判斷參數對應的數據是否存在,存在就返回對應的結果數據。
第一版我們來寫一版:
// 第一版 (來自《JavaScript權威指南》) function memoize(f) { var cache = {}; return function(){ var key = arguments.length + Array.prototype.join.call(arguments, ","); if (key in cache) { return cache[key] } else return cache[key] = f.apply(this, arguments) } }
我們來測試一下:
var add = function(a, b, c) { return a + b + c } var memoizedAdd = memorize(add) console.time("use memorize") for(var i = 0; i < 100000; i++) { memoizedAdd(1, 2, 3) } console.timeEnd("use memorize") console.time("not use memorize") for(var i = 0; i < 100000; i++) { add(1, 2, 3) } console.timeEnd("not use memorize")
在 Chrome 中,使用 memorize 大約耗時 60ms,如果我們不使用函數記憶,大約耗時 1.3 ms 左右。
注意什么,我們使用了看似高大上的函數記憶,結果卻更加耗時,這個例子近乎有 60 倍呢!
所以,函數記憶也并不是萬能的,你看這個簡單的場景,其實并不適合用函數記憶。
需要注意的是,函數記憶只是一種編程技巧,本質上是犧牲算法的空間復雜度以換取更優的時間復雜度,在客戶端 JavaScript 中代碼的執行時間復雜度往往成為瓶頸,因此在大多數場景下,這種犧牲空間換取時間的做法以提升程序執行效率的做法是非常可取的。
第二版因為第一版使用了 join 方法,我們很容易想到當參數是對象的時候,就會自動調用 toString 方法轉換成 [Object object],再拼接字符串作為 key 值。我們寫個 demo 驗證一下這個問題:
var propValue = function(obj){ return obj.value } var memoizedAdd = memorize(propValue) console.log(memoizedAdd({value: 1})) // 1 console.log(memoizedAdd({value: 2})) // 1
兩者都返回了 1,顯然是有問題的,所以我們看看 underscore 的 memoize 函數是如何實現的:
// 第二版 (來自 underscore 的實現) var memorize = function(func, hasher) { var memoize = function(key) { var cache = memoize.cache; var address = "" + (hasher ? hasher.apply(this, arguments) : key); if (!cache[address]) { cache[address] = func.apply(this, arguments); } return cache[address]; }; memoize.cache = {}; return memoize; };
從這個實現可以看出,underscore 默認使用 function 的第一個參數作為 key,所以如果直接使用
var add = function(a, b, c) { return a + b + c } var memoizedAdd = memorize(add) memoizedAdd(1, 2, 3) // 6 memoizedAdd(1, 2, 4) // 6
肯定是有問題的,如果要支持多參數,我們就需要傳入 hasher 函數,自定義存儲的 key 值。所以我們考慮使用 JSON.stringify:
var memoizedAdd = memorize(add, function(){ var args = Array.prototype.slice.call(arguments) return JSON.stringify(args) }) console.log(memoizedAdd(1, 2, 3)) // 6 console.log(memoizedAdd(1, 2, 4)) // 7
如果使用 JSON.stringify,參數是對象的問題也可以得到解決,因為存儲的是對象序列化后的字符串。
適用場景我們以斐波那契數列為例:
var count = 0; var fibonacci = function(n){ count++; return n < 2? n : fibonacci(n-1) + fibonacci(n-2); }; for (var i = 0; i <= 10; i++){ fibonacci(i) } console.log(count) // 453
我們會發現最后的 count 數為 453,也就是說 fibonacci 函數被調用了 453 次!也許你會想,我只是循環到了 10,為什么就被調用了這么多次,所以我們來具體分析下:
當執行 fib(0) 時,調用 1 次 當執行 fib(1) 時,調用 1 次 當執行 fib(2) 時,相當于 fib(1) + fib(0) 加上 fib(2) 本身這一次,共 1 + 1 + 1 = 3 次 當執行 fib(3) 時,相當于 fib(2) + fib(1) 加上 fib(3) 本身這一次,共 3 + 1 + 1 = 5 次 當執行 fib(4) 時,相當于 fib(3) + fib(2) 加上 fib(4) 本身這一次,共 5 + 3 + 1 = 9 次 當執行 fib(5) 時,相當于 fib(4) + fib(3) 加上 fib(5) 本身這一次,共 9 + 5 + 1 = 15 次 當執行 fib(6) 時,相當于 fib(5) + fib(4) 加上 fib(6) 本身這一次,共 15 + 9 + 1 = 25 次 當執行 fib(7) 時,相當于 fib(6) + fib(5) 加上 fib(7) 本身這一次,共 25 + 15 + 1 = 41 次 當執行 fib(8) 時,相當于 fib(7) + fib(6) 加上 fib(8) 本身這一次,共 41 + 25 + 1 = 67 次 當執行 fib(9) 時,相當于 fib(8) + fib(7) 加上 fib(9) 本身這一次,共 67 + 41 + 1 = 109 次 當執行 fib(10) 時,相當于 fib(9) + fib(8) 加上 fib(10) 本身這一次,共 109 + 67 + 1 = 177 次
所以執行的總次數為:177 + 109 + 67 + 41 + 25 + 15 + 9 + 5 + 3 + 1 + 1 = 453 次!
如果我們使用函數記憶呢?
var count = 0; var fibonacci = function(n) { count++; return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); }; fibonacci = memorize(fibonacci) for (var i = 0; i <= 10; i++) { fibonacci(i) } console.log(count) // 12
我們會發現最后的總次數為 12 次,因為使用了函數記憶,調用次數從 453 次降低為了 12 次!
興奮的同時不要忘記思考:為什么會是 12 次呢?
從 0 到 10 的結果各儲存一遍,應該是 11 次吶?咦,那多出來的一次是從哪里來的?
所以我們還需要認真看下我們的寫法,在我們的寫法中,其實我們用生成的 fibonacci 函數覆蓋了原本了 fibonacci 函數,當我們執行 fibonacci(0) 時,執行一次函數,cache 為 {0: 0},但是當我們執行 fibonacci(2) 的時候,執行 fibonacci(1) + fibonacci(0),因為 fibonacci(0) 的值為 0,!cache[address] 的結果為 true,又會執行一次 fibonacci 函數。原來,多出來的那一次是在這里!
多說一句也許你會覺得在日常開發中又用不到 fibonacci,這個例子感覺實用價值不高吶,其實,這個例子是用來表明一種使用的場景,也就是如果需要大量重復的計算,或者大量計算又依賴于之前的結果,便可以考慮使用函數記憶。而這種場景,當你遇到的時候,你就會知道的。
專題系列JavaScript專題系列目錄地址:https://github.com/mqyqingfeng/Blog。
JavaScript專題系列預計寫二十篇左右,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特點是研(chao)究(xi) underscore 和 jQuery 的實現方式。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88389.html
摘要:專題系列共計篇,主要研究日常開發中一些功能點的實現,比如防抖節流去重類型判斷拷貝最值扁平柯里遞歸亂序排序等,特點是研究專題之函數組合專題系列第十六篇,講解函數組合,并且使用柯里化和函數組合實現模式需求我們需要寫一個函數,輸入,返回。 JavaScript 專題之從零實現 jQuery 的 extend JavaScritp 專題系列第七篇,講解如何從零實現一個 jQuery 的 ext...
摘要:寫在前面專題系列是我寫的第二個系列,第一個系列是深入系列。專題系列自月日發布第一篇文章,到月日發布最后一篇,感謝各位朋友的收藏點贊,鼓勵指正。 寫在前面 JavaScript 專題系列是我寫的第二個系列,第一個系列是 JavaScript 深入系列。 JavaScript 專題系列共計 20 篇,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里...
摘要:專題系列第十八篇,講解遞歸和尾遞歸定義程序調用自身的編程技巧稱為遞歸。然而非尾調用函數,就會創建多個執行上下文壓入執行上下文棧。所以我們只用把階乘函數改造成一個尾遞歸形式,就可以避免創建那么多的執行上下文。 JavaScript 專題系列第十八篇,講解遞歸和尾遞歸 定義 程序調用自身的編程技巧稱為遞歸(recursion)。 階乘 以階乘為例: function factorial(n...
摘要:前言在計算機領域,記憶是主要用于加速程序計算的一種優化技術,它使得函數避免重復演算之前已被處理過的輸入,而返回已緩存的結果。被執行了不是素數,其他數字默認是素數。我們可以看出,如果從開始打印斐波那契數列,函數被執行了次。 前言 在計算機領域,記憶(memoization)是主要用于加速程序計算的一種優化技術,它使得函數避免重復演算之前已被處理過的輸入,而返回已緩存的結果。 -- wi...
摘要:為此決定自研一個富文本編輯器。例如當要轉化的對象有環存在時子節點屬性賦值了父節點的引用,為了關于函數式編程的思考作者李英杰,美團金融前端團隊成員。只有正確使用作用域,才能使用優秀的設計模式,幫助你規避副作用。 JavaScript 專題之惰性函數 JavaScript 專題系列第十五篇,講解惰性函數 需求 我們現在需要寫一個 foo 函數,這個函數返回首次調用時的 Date 對象,注意...
閱讀 3187·2021-11-23 09:51
閱讀 1524·2021-11-22 09:34
閱讀 2835·2021-10-27 14:15
閱讀 2265·2021-10-12 10:17
閱讀 1883·2021-10-12 10:12
閱讀 946·2021-09-27 14:00
閱讀 1995·2021-09-22 15:19
閱讀 1032·2019-08-30 10:51