函數尾調用就是指函數的最後一步是調用另外一個函數。算法
1 //函數尾調用示例一 2 function foo(x){ 3 return g(x); 4 } 5 //函數尾調用示例二 6 function fun(x){ 7 if(x > 0){ 8 return g(x); 9 } 10 return y(x); 11 }
調用最後一步和最後一行代碼的區別,最後一步的代碼並不必定會在最後一行,好比示例二。還有下面這一種不能叫作函數尾調用:瀏覽器
1 // 下面這種狀況不叫作函數尾調用 2 function fu(x){ 3 var y = 10 * x; 4 g(y); 5 }
爲何這種狀況不叫做函數的尾調用呢?緣由很簡單,由於函數執行的最後一步是return指令,這個指令有兩個功能,第一個功能是結束當前函數執行,第二個功能是將指令後面的數據傳遞出去(函數返回值)。而這個return指令無論有沒有聲明都會執行,沒有聲明的狀況下返回的數據是undefined,因此上面的代碼其實是如下結構:函數
1 function fu(x){ 2 var y = 10 * x; 3 g(y); 4 return undefined; 5 }
return指令是先關閉函數,而後再返回數據。說到這裏,就會引起一個問題出來,若是最後一步不是函數尾調用會怎麼樣?return指令後面是下面這種狀況,會發生什麼?spa
1 //數的階乘 2 function factorial(n){ 3 if(n === 1 || n ===0 ) return 1; 4 return n * factorial(n - 1); 5 }
上面這個數的階乘算法示例不能叫作函數尾調用,由於最後一步是乘積計算,不是純粹的函數調用。code
尾調用本質上就是說函數最後執行的一步return指令中,返回數據的這一部分是一個函數執行。看似這個簡單的指令和其簡單明瞭的功能,並無特別之處。可是函數執行時,會在內存造成一個「調用記錄」,一般被稱爲「調用幀」。注意,是在函數執行時內部調用,也就是說是在return指令觸發以前的函數調用,由於return指令以後的函數調用會產生一個獨立的函數調用棧,而不是在原來的函數調用棧上添加調用幀。blog
咱們直到瀏覽器分配的內存空間是有限的資源,也就是說函數的調用棧內存是有限的,若是函數出現很大的循環嵌套調用函數,每一個嵌套的函數調用都會在原來的函數調用棧頂上添加一個調用幀,像上面的數的階乘若是傳入的參數是100的話,就會在factorial函數調用棧上產生99個調用幀,若是實參再大一點呢?1000或者更多,這種無限堆疊的可能確定會帶來一個風險,就是棧溢出。遞歸
再來看下面這個示例:索引
1 function fb(n){ 2 if(n == 1 || n == 2){ 3 return 1 4 } 5 return fb(n - 1) + fb(n - 2); 6 } 7 console.log(fb(100)); //堆棧溢出,瀏覽器崩潰
上面這個示例(斐波那契數列)有跟乘介算法同樣的問題,就是都是在return指令後面對函數執行結果在計算,而這種計算實際上發生當前函數上,並且還會在函數的調用棧上不斷增長調用幀,直到符合程序出口邏輯纔會中止。可是當計算的數值達到必定程度時就會致使堆棧溢出,形成瀏覽器奔潰。內存
說了這麼多,一直沒有明確解析什麼是尾遞歸,其實沒什麼能夠解析的,就是在return指令後面調用自身函數執行。而後下面就是使用尾遞歸和ES的默認參數解決階乘和斐波那契數列算法的調用幀溢出問題:資源
1 //使用ES6的默認值 + 尾遞歸實現階乘算法 2 function factorial1(n,total=1){ 3 if(n === 1 || n === 0 ) return total; 4 n += 1; 5 return factorial(n - 1, n * total); 6 } 7 //使用ES6的默認值 + 尾遞歸實現斐波那契數列數列算法 8 function fb1(n, ac1 = 1, ac2 = 1){ 9 if( n === 1 || n === 2) return ac2; 10 return fb1 (n - 1, ac2, ac1 + ac2); 11 }
在阮一峯老師的《ES6標準入門第三版》P127,中發現老師的兩個算法在計算上值都少計算一位,好比老師的階乘計算5的階乘結果是24,這個結果一開始令我迷惑不解,我的推斷老師的思路是按照計算機的計數方式(從0開始),其參數指定的是階乘結果的索引,採用參數指定計算值所在結果集合的索引。不知道這個推測是否正確,若是有不對的地方還請各位指正。
而我在示例中採用的是數值的階乘結果,不是階乘結果表中的索引。