尾呼叫
前一段時間偶然情況下了解到了尾呼叫這個概念,然後就去了解了一下,其實用程式碼來解釋是非常容易的:
function foo(x){
return x+1
}
function ex(){
var num = 2
return foo(num)
}
//尾呼叫不一定出現在函式尾部,只要是最後一步操作即可。
//當一個函式的最後一步是另一個函式的呼叫(只是某個函式的呼叫),那麼,這種情況就稱之為尾呼叫
複製程式碼
尾呼叫為什麼會單獨拿出來作為一個概念並且被討論,究其原因是函式呼叫會在記憶體中形成一個呼叫幀(用以儲存呼叫位置,上下文變數等資訊),如果在函式A內部呼叫了函式B,那麼,會在A的呼叫幀上面再記錄一個B的呼叫幀,等B執行結束將結果返回給A之後,B的呼叫幀消失;如果B內部還呼叫了函式C,那麼,C的呼叫幀。還會出現在B的上方。這就是一個壓棧的過程,因此呼叫過程其實是形成了一個呼叫棧的。 尾呼叫由於是函式的最後一步操作,所以不需要保留外層函式的呼叫記錄,因為呼叫位置、內部變數等資訊都不會再用到了,只要直接用內層函式的呼叫記錄,取代外層函式的呼叫記錄就可以了。
優化
優化方式根據使用的語言不同則有不同實現,此處拿JavaScript來舉例
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同於
function f() {
return g(3);
}
f();
// 等同於
g(3);
//我們可以有意識的將函式簡化或改寫成非尾呼叫的形式
複製程式碼
上面程式碼中,如果函式g不是尾呼叫,函式f就需要儲存內部變數m和n的值、g的呼叫位置等資訊。但由於呼叫g之後,函式f就結束了,所以執行到最後一步,完全可以刪除 f() 的呼叫記錄,只保留 g(3) 的呼叫記錄。
這就叫做"尾呼叫優化"(Tail call optimization),即只保留內層函式的呼叫記錄。如果所有函式都是尾呼叫,那麼完全可以做到每次執行時,呼叫記錄只有一項,這將大大節省記憶體。這就是"尾呼叫優化"的意義。
尾遞迴
尾呼叫的是自身,被稱為尾遞迴。 遞迴非常耗費記憶體,因為需要同時儲存成千上百個呼叫記錄,很容易發生"棧溢位"錯誤(stack overflow)。但對於尾遞迴來說,由於只存在一個呼叫記錄,所以永遠不會發生"棧溢位"錯誤。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
複製程式碼
上面程式碼是一個階乘函式,計算n的階乘,最多需要儲存n個呼叫記錄,複雜度 O(n) 。
如果改寫成尾遞迴,只保留一個呼叫記錄,複雜度 O(1) 。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
複製程式碼
由此可見,"尾呼叫優化"對遞迴操作意義重大,所以一些函數語言程式設計語言將其寫入了語言規格。ES6也是如此,第一次明確規定,所有 ECMAScript 的實現,都必須部署"尾呼叫優化"。這就是說,在 ES6 中,只要使用尾遞迴,就不會發生棧溢位,相對節省記憶體,ES6的尾呼叫優化只在嚴格模式下開啟,正常模式是無效的。(參考自阮一峰:www.ruanyifeng.com/blog/2015/0…)