讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議65:比較函數的惰性求值與非惰性求值 >

建議65:比較函數的惰性求值與非惰性求值

在JavaScript中,使用函數式風格編程時,應該對於表達式有著深刻的理解,並能夠主動使用表達式的連續運算來組織代碼。

1)在運算元中,除了JavaScript默認的數據類型外,函數也作為一個重要的運算元參與運算。

2)在運算符中,除了JavaScript的大量預定義運算符外,函數還作為一個重要的運算符進行計算和組織代碼。

函數作為運算符參與運算,具有非惰性求值特性。非惰性求值行為自然會對整個程序產生一定的負面影響。先看下面這個示例:


var a=2;

function f(x){

return x;

}

alert(f(a,a=a*a));//2

alert(f(a));//4


在上面的示例中,兩次調用同一個函數並傳遞同一個變量,所返回的值卻不一樣。在第一次調用函數時,向其傳遞了兩個參數,第二個參數是一個表達式,該表達式對變量a進行重新計算和賦值。也就是說,當調用函數時,第二個參數雖然不使用,但是也被計算了。這就是JavaScript的非惰性求值特性,也就是說,不管表達式是否被利用,只要在執行代碼行中都會被計算。

如果在一個函數參數中無意添加了幾個表達式,雖然這樣不會對函數的運算結果產生影響,但是由於表達式被執行,就會對整個程序產生潛在的負面影響。

在惰性求值語言中,如果參數不被調用,那麼無論參數是直接量還是某個表達式,都不會佔用系統資源。但是,由於JavaScript支持非惰性求值,問題就變得很特殊了。


function f{}

f(function{while(true);})


在上面的示例中,雖然函數f沒有參數,但是在調用時將會執行傳遞給它的參數表達式,該表達式是一個死循環結構的函數值,最終將導致系統崩潰。

惰性函數模式是一種將對函數或請求的處理延遲到真正需要結果時進行的通用概念,很多應用程序都採用了這種概念。從惰性編程的角度來思考問題,可以幫助消除代碼中不必要的計算。例如,在Scheme語言中,delay特殊表單接收一個代碼塊,它不會立即執行這個代碼塊,而是將代碼和參數作為一個promise存儲起來。如果需要promise產生一個值,就會運行這段代碼。promise隨後會保存結果,這樣將來再請求這個值時,該值就可以立即返回,而不用再次執行代碼。這種設計模式在JavaScript中大有用處,尤其是在編寫跨瀏覽器的、高效運行的庫時非常有用。例如,下面是一個時間對像實例化的函數。


var t;

function f{

t=t?t:new Date;

return t;

}

f;//調用函數


上面的示例使用全局變量t來存儲時間對象,這樣在每次調用函數時都必須進行重新求值,代碼的效率沒有得到優化,同時全局變量t很容易被所有代碼訪問和操作,存在安全隱患。當然,可以使用閉包隱藏全局變量t,只允許在函數f內訪問。


var f=(function{

var t;

return function{

t=t?t:new Date;

return t;

}

});

f;


這仍然沒有提高調用時的效率,因為每次調用f依然需要求值:


var f=function{

var t=new Date;

f=function{

return t;

}

return f;

};

f;


在上面的示例中,函數f的首次調用將實例化一個新的Date對象並重置f到一個新的函數上,f在其閉包內包含Date對象。在首次調用結束之前,f的新函數值也已被調用並提供返回值。

函數f的調用都只會簡單地返回t保留在其閉包內的值,這樣執行起來非常高效。弄清這種模式的另一種途徑是,外部函數f的首次調用是一個保證(promise),它保證了首次調用會重定義f為一個非常有用的函數,保證來自於Scheme的惰性求值機制。