function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120
上面代碼是一個階乘函數,計算n
的階乘,最多須要保存n
個調用記錄,複雜度 O(n) 。javascript
若是改寫成尾遞歸,只保留一個調用記錄,複雜度 O(1) 。java
函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。編程
遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用幀,因此永遠不會發生「棧溢出」錯誤編程語言
function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120
還有一個比較著名的例子,就是計算 Fibonacci 數列,也能充分說明尾遞歸優化的重要性。函數式編程
非尾遞歸的 Fibonacci 數列實現以下。函數
function Fibonacci (n) { if ( n <= 1 ) {return 1}; return Fibonacci(n - 1) + Fibonacci(n - 2); } Fibonacci(10) // 89 Fibonacci(100) // 堆棧溢出 Fibonacci(500) // 堆棧溢出
尾遞歸優化過的 Fibonacci 數列實現以下。優化
unction Fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 + ac2); } Fibonacci2(100) // 573147844013817200000 Fibonacci2(1000) // 7.0330367711422765e+208 Fibonacci2(10000) // Infinity
因而可知,「尾調用優化」對遞歸操做意義重大,因此一些函數式編程語言將其寫入了語言規格。ES6 是如此,第一次明確規定,全部 ECMAScript 的實現,都必須部署「尾調用優化」。這就是說,ES6 中只要使用尾遞歸,就不會發生棧溢出,相對節省內存。code
遞歸函數的改寫blog
function factorial(n, total = 1) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5) // 120