兩個很常見的遞歸函數:
// 階乘
function factorial(n) {
if (n == 1) return n;
return n * factorial(n - 1)
}
console.log(factorial(5)) // 5 * 4 * 3 * 2 * 1 = 120
// 斐波那契數列
function fibonacci(n){
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(5)) // 1 1 2 3 5
遞歸的特點其實就是:
第一,將原始問題拆分,並且拆分得到的子問題和原始問題實現相同的功能。
第二,必須有一個出口讓遞歸函數結束,否則遞歸將要無限執行。
遞歸函數存在一個問題:
當JS執行一個函數的時候,就會創建一個執行上下文。然後,這個執行上下文就會被壓入執行上下文棧。如果遞歸不停的調用自身,那麼執行上下文棧也就會越來越大。醬紫不太好對吧。
尾調用
這就是解決方案。
尾調用是什麼:
指的就是,這個函數的最後一步是執行一個函數,並且,該被執行的函數的返回值就是本函數的返回值。
// 尾調用
function f(x){
return g(x); // 最後一個動作是執行g函數
}
// 非尾調用
function f(x){
return g(x) + 1; // 執行g函數後又執行了一步加法
}
那麼第一個尾調用的執行上下文變化:
// 僞代碼
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.push(<g> functionContext);
ECStack.pop();
我們來解釋一下,爲什麼會這樣:
因爲函數的調用棧還有執行上下文是爲了保存函數內部狀態的,尾調用由於是函數的最後一步操作,所以不需要保留外層函數的調用記錄,因爲調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用記錄,取代外層函數的調用記錄就可以了。
但是,如果函數 g 中用到了f函數中的變量,形成閉包,f 就不會被彈出。
第二個則是:
ECStack.push(<f> functionContext);
ECStack.push(<g> functionContext);
ECStack.pop();
ECStack.pop();
我們可以看到使用了尾調用的遞歸函數,在上下文棧中只有一個函數入棧。簡直厲害!
函數調用自身,稱爲遞歸。如果尾調用自身,就稱爲尾遞歸。
優化
下面,我們就來優化剛纔的階乘函數。
function factorial(n, res) {
if (n == 1) return res;
return factorial(n - 1, n * res)
}
console.log(factorial(4, 1)) // 24
多了一個參數,不過我們的上下文棧保持了乾淨。
新技能get了嘛~