尾調用(Tail Call)是函數式編程的一個重要概念,自己很是簡單,一句話就能說清楚,就是指某個函數的最後一步是調用另外一個函數。編程
//尾調用 function f(x) {return g(x);} //上面代碼中,函數f的最後一步是調用函數g,這就叫尾調用 //不屬於尾調用舉例 // 狀況一 function f(x){ let y = g(x); return y; } // 狀況二 function f(x){ return g(x) + 1; } // 狀況三 function f(x){ g(x); }
尾調用之因此與其餘調用不一樣,就在於它的特殊的調用位置。函數式編程
咱們知道,函數調用會在內存造成一個「調用記錄」,又稱「調用幀」(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用幀上方,還會造成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用幀纔會消失。若是函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。全部的調用幀,就造成一個「調用棧」(call stack)。函數
尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用幀,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就能夠了。優化
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(x) 的調用幀,只保留 g(3) 的調用幀。this
這就叫作「尾調用優化」(Tail call optimization),即只保留內層函數的調用幀。若是全部函數都是尾調用,那麼徹底能夠作到每次執行時,調用幀只有一項,這將大大節省內存。這就是「尾調用優化」的意義。spa
注意,只有再也不用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀,不然就沒法進行「尾調用優化」。code
函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。blog
遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用幀,因此永遠不會發生「棧溢出」錯誤。 以斐波那契數列(Fibonacci)爲例分析:遞歸
//普通遞歸 function Fibonacci (n) { if ( n <= 1 ) {return 1}; return Fibonacci(n - 1) + Fibonacci(n - 2); } Fibonacci(10); // 89 // Fibonacci(100) // Fibonacci(500) // 堆棧溢出了 //尾遞歸 function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 + ac2); //保留一個調用記錄,複雜度O(1) } Fibonacci2(100) // 573147844013817200000 Fibonacci2(1000) // 7.0330367711422765e+208 Fibonacci2(10000) // Infinity
尾遞歸的實現,每每須要改寫遞歸函數,確保最後一步只調用自身。作到這一點的方法,就是把全部用到的內部變量改寫成函數的參數。這樣作的缺點是不太直觀,解決辦法有兩種:內存
方法1:在尾遞歸函數以外,再提供一個正常形式的函數。
//尾遞歸 function 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 //優化1 function tailFibonacci(n, ac1, ac2){ if( n <= 1 ) {return ac2}; return tailFibonacci (n - 1, ac2, ac1 + ac2); } function Fibonacci3(n){ return tailFibonacci(n, 1 , 1); } Fibonacci3(100) // 573147844013817200000
方法2:函數式編程有一個概念,叫作柯里化(currying),意思是將多參數的函數轉換成單參數的形式。這裏也可使用柯里化。
//優化2 柯里化 function currying(fn, n1, n2) { return function (m) { return fn.call(this, m, n1, n2); }; } function tailFibonacci(n, ac1, ac2){ if( n <= 1 ) {return ac2}; return tailFibonacci (n - 1, ac2, ac1 + ac2); } const Fibonacci4 = currying(tailFibonacci,1,1); Fibonacci4(100) // 573147844013817200000