前一段時間偶然狀況下了解到了尾調用這個概念,而後就去了解了一下,其實用代碼來解釋是很是容易的:html
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) 。spa
若是改寫成尾遞歸,只保留一個調用記錄,複雜度 O(1) 。code
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…)htm