你知道關於前端的記憶函數嗎?

函數能夠將以前的操做結果緩存在某個對象中,當下次調用時,若是遇到相同的參數,就直接返回緩存中的數據,從而避免無謂的重複運算。這種優化被稱做記憶。javascript

什麼是函數記憶

函數能夠將以前的操做結果緩存在某個對象中,當下次調用時,若是遇到相同的參數,就直接返回緩存中的數據,從而避免無謂的重複運算。這種優化被稱做記憶。java

舉個例子:算法

function add(a, b) {
    return a + b;
}

// 假設 memoize 能夠實現函數記憶
let memoizeAdd = memoize(add);

memoizeAdd(1, 2) // 3
memoizeAdd(1, 2) // 相同的參數,第二次調用時,從緩存中取出數據,而非從新計算一次
複製代碼

記憶只是一種編程技巧,本質上是犧牲算法的空間複雜度以換取更優的時間複雜度,在客戶端 Javascript 中代碼的執行時間複雜度每每成爲瓶頸,所以在大多數狀況下,這種犧牲空間換取時間的作法是很是可取的。編程

適用場景

好比說,咱們想要一個遞歸函數來計算 Fibonacci 數列。 一個 Fibonacci 數字是以前兩個 Fibonacci 數字之和。 最前面的兩個數組是 0 和 1。數組

let count = 0; //用於記錄函數調用次數
let fibonacci = function(n){
    count ++ ; // 每次調用函數將 count + 1
    return n < 2 ? n : fibonacci(n-1) + fibonacci(n - 2);
}

for(let i = 0; i < 10; i++){
    console.log(`i=${i}:`,fibonacci(i))
}

console.log('總次數:',count)

// i=0:0
// i=1:1
// i=2:1
// i=3:2
// i=4:3
// i=5:5
// i=6:8
// i=7:13
// i=8:21
// i=9:34

// 總次數: 276
複製代碼

上面的代碼自己是沒什麼問題的,但它作了不少無謂的工做。咱們在 for 循環中共調用了 10 次 fibonacci 函數,但實際上 fibonacci 函數被調用了 276 次,它自身調用了 266 次去計算可能已被剛剛計算過的值。若是咱們讓該函數具有記憶功能,就能夠顯著地減小運算量。緩存

實現

接下來咱們來思考如何實現一個通用函數( memoize )來幫助咱們構造帶記憶功能的函數。閉包

原理上很簡單,只是將函數的參數和對應的結果一併緩存至閉包中,待調用時判斷參數對應的數據是否存在,存在就直接返回緩存的數據結果。app

代碼實現以下:函數

let memoize = function(fn){
    let cache = {};
    return function(...args){
        let key = JSON.stringify(args);
        if(!cache.hasOwnProperty(key)){
            cache[key] = fn.apply(this,args);
        }
        return cache[key];
    };
}
複製代碼

上面的代碼,咱們將函數的參數轉換爲JSON字符串後用做緩存的 key,這以基本可以保證每次函數調用可經過參數獲取精確的 Key 值。優化

但對於一些特殊的參數經過 JSON.stringify 轉換後,並不能得到真實的 key 值,好比 undefined、NaN、Infinity、正則對象、函數等。

考慮給 memoize 函數增長一個函數類型的參數 resolver ,用於將緩存的 key 的生成規則轉交給用戶。

實現以下:

let memoize = function(fn,resolver){
    let cache = {};
    return function(...args){
        let key = typeof resolver === 'function' ? resolver.apply(this,args) :JSON.stringify(args);
        if(!cache.hasOwnProperty(key)){
            cache[key] = fn.apply(this,args);
        }
        return cache[key];
    };
}
複製代碼

驗證

依然使用 Fibonacci 的例子來驗證一下咱們完成的 memoize 函數。

函數調用次數是否減小

let count = 0; //用於記錄函數調用次數
let fibonacci = function(n){
    count ++ ; // 每次調用函數將 count + 1
    return n < 2 ? n : fibonacci(n-1) + fibonacci(n - 2);
}

fibonacci = memoize(fibonacci);

for(let i = 0; i < 10; i++){
    console.log(`i=${i}:`,fibonacci(i))
}

console.log('總次數:',count)

// i=0: 0
// i=1: 1
// i=2: 1
// i=3: 2
// i=4: 3
// i=5: 5
// i=6: 8
// i=7: 13
// i=8: 21
// i=9: 34

// 總次數: 10
複製代碼

函數調用時間是否減小了

未使用 memoize 時,n 爲 30 時 fibonacci 函數執行時間以下:

console.time('no memoize time');
fibonacci(30);
console.timeEnd('no memoize time');

// no memoize time: 10.919ms
複製代碼

使用 memoize 時,n 爲 30 時 fibonacci 函數執行時間以下:

fibonacci = memoize(fibonacci);

console.time('memoize time');
fibonacci(30);
console.timeEnd('memoize time');

// memoize time: 0.331ms
複製代碼

兩者時間比較,咱們能夠很清晰的看到使用 memoize 函數後,函數的調用時間大幅下降。

總結

  1. 函數記憶:
    • 讓函數記住處理過的參數和處理結果
  2. 函數記憶的做用:
    • 爲避免重複運算
  3. 何時使用函數記憶:
    • 函數可能反覆計算相同的數據時
  4. 如何實現:
    • 使用閉包保存住曾經計算過的參數和處理結果

喜歡嗎?別忘了給聲先生點個贊👍

相關文章
相關標籤/搜索