比如說,我們想要一個遞迴函式來計算 Fibonacci 數列。一個 Fibonacci 數字是之前兩個 Fibonacci 數字之和。最前面的兩個數字是 0 和 1。
複製代碼 代碼如下:var fibonacci = function (n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};
for (var i = 0; i <= 10; i += 1) {
document.writeln('// ' + i + ': ' + fibonacci(i));
}
// 0: 0
// 1: 1
// 2: 1
// 3: 2
// 4: 3
// 5: 5
// 6: 8
// 7: 13
// 8: 21
// 9: 34
// 10: 55
這樣是可以工作的,但是它做了很多無謂的工作。 Fibonacci 函數被調用了 453 次。我們調用了 11 次,而它自身調用了 442 次去計算可能已經被剛計算過的值。如果我們讓該函數具備記憶功能,就可以顯著地減少它的運算量。
我們在一個名為 memo 的數組裡儲存我們的儲存結果,儲存結果可以隱藏在閉包中。當我們的函數被調用時,這個函數首先看是否已經知道計算的結果,如果已經知道,就立即返回這個儲存結果。 複製代碼 代碼如下:var fibonacci = function() {
var memo = [0, 1];
var fib = function (n) {
var result = memo[n];
if (typeof result !== 'number') {
result = fib(n - 1) + fib(n - 2);
memo[n] = result;
}
return result;
};
return fib;
}();
這個函數返回同樣的結果,但是它只被調用了 29 次。我們調用了它 11 次,它自身調用了 18 次去取得之前儲存的結果。
以上內容來自:http://demon.tw/programming/javascript-memoization.html
realazy在blog上給出了一個JavaScript Memoization的實現,Memoization就是函數傳回值的緩衝,比如一個函數參數與返回結果一一對應的hash列表,wiki上其實也有詳細解釋,我不細說了,只討論一下具體實現的問題,realazy文中的代碼有一些問題,比如直接用參數拼接成的字串作為查詢快取結果的key,如果參數裡包括對象或數組的話,就很難保證唯一的key,還有1樓評論裡提到的:[221,3]和[22,13]這樣的參數也無法區分。
那麼來改寫一下,首先還是用hash表來存放快取資料:
複製代碼 代碼如下:function Memoize(fn){
var cache = {};
return function(){
var key = [];
for( var i=0, l = arguments.length; i < l; i++ )
key.push(arguments[i]);
if( !(key in cache) )
cache[key] = fn.apply(this, arguments);
return cache[key];
};
}
嗯,區別是直接把數組當作鍵來用,不過要注意函數裡的arguments是js解譯器實現的一個特殊對象,並不是真正的數組,所以要轉換一下……
ps: 原來的參數包括方法名稱和上下文引用:fib.fib_memo = Memoize(‘fib_memo', fib),但實際上currying產生的函數裡可以用this直接引用上層對象,更複雜的例子可以參考John Resig的makeClass,所以我改成直接傳函數引用:fib.fib_memo = Memoize(fib.fib_memo)
這樣寫看上去似乎很靠譜,由參數組成的數組不是唯一的麼。但實際上,數組之所以能作為js對象的屬性名稱來使用,是因為它被當作字串處理了,也就是說如果你給函數傳的參數是這樣:(1,2,3), cache對象就會是這個樣子:{ “1,2,3″: somedata },如果你的參數裡有對象,比如:(1,2,{i:”yy”}),實際的索引值會是:”1,2,[object Object]“,所以這跟把數組拼接成字串的方法其實沒有區別……
樣本: 複製代碼 代碼如下:var a = [1,2,{yy:'0'}];
var b = [1,2,{xx:'1'}];
var obj = {};
obj[a] = "111";
obj[b] = "222";
for( var i in obj )
alert( i + " = " + obj[i] ); //只會彈出"1,2,[object Object] = 222",obj[a] = "111"被覆蓋了
直接用參數作為鍵名的方法不靠譜了…………換一種方法試試: 複製代碼 代碼如下:function Memoize(fn){
var cache = {}, args = [];
return function(){
for( var i=0, key = args.length; i < key; i++ ) {
if( equal( args[i], arguments ) )
return cache[i];
}
args[key] = arguments;
cache[key] = fn.apply(this, arguments);
return cache[key];
};
}
可以完全避免上述問題,沒有使用hash的索引值對索引,而是把函數的參數和結果分別緩衝在兩個列表裡,每次都先遍曆整個參數列表作比較,找出對應的鍵名/ID號之後再從結果清單裡取資料。以下是比較數組的equal方法: 複製代碼 代碼如下:function equal( first, second ){
if( !first || !second || first.constructor != second.constructor )
return false;
if( first.length && typeof first != "string" )
for(var i=0, l = ( first.length > second.length ) ? first.length : second.length; i<l; i++){
if( !equal( first[i], second[i] ) ) return false;
}
else if( typeof first == 'object' )
for(var n in first){
if( !equal( first[n], second[n] ) ) return false;
}
else
return ( first === second );
return true;
}
千萬不要直接用==來比較arguments和args裡的數組,那樣比較的是記憶體引用,而不是參數的內容。
這種方法的速度很慢,equal方法其實影響不大,但是緩衝的結果數量多了以後,每次都要遍曆參數列表卻是很沒效率的(求80以上的fibonacci數列,在firefox3和safari3上都要40ms左右)
如果在實際應用中參數變動不多或者不接受參數的話,可以參考Oliver Steel的這篇《One-Line JavaScript Memoization》,用很短的函數式風格解決問題: 複製代碼 代碼如下:function Memoize(o, p) {
var f = o[p], mf, value;
var s = function(v) {return o[p]=v||mf};
((mf = function() {
(s(function(){return value})).reset = mf.reset;
return value = f.apply(this,arguments); //此處修改過,允許接受參數
}).reset = s)();
}
樣本: 複製代碼 代碼如下:var fib = {
temp: function(n){
for(var i=0;i<10000;i++)
n=n+2;
return n;
}
}
Memoize(fib,"temp"); //讓fib.temp緩衝傳回值
fib.temp(16); //執行結果:20006,被緩衝
fib.temp(20); //執行結果:20006
fib.temp(10); //執行結果:20006
fib.temp.reset(); //重設緩衝
fib.temp(10); //執行結果:20010