這是我在學習函數式編程的時候,關於遞歸,尾遞歸,相互遞歸和蹦牀函數的一些心得,記下來供之後開發時參考,相信對其餘人也有用。javascript
參考資料:JavaScript玩轉Clojure大法之 - Trampolinehtml
咱們知道,es5是沒有尾遞歸優化的,因此在遞歸的時候,若是層數太多,就會報「Maximum call stack size exceeded」的錯誤。就連下面這個及其簡單的遞歸函數都會報「Maximum call stack size exceeded」的錯誤。java
function haha(a) { if(!a) return a; return haha(a-1); } haha(100); //輸出0 haha(12345678); //輸出「Maximum call stack size exceeded」
爲何會報「Maximum call stack size exceeded」的錯誤?我以爲緣由是在每次遞歸調用的時候,會把當前做用域裏面的基本類型的值推動棧中,因此一旦遞歸層數過多,棧就會溢出,因此會報錯。編程
注意:閉包
怎麼優化上面的狀況呢?方法是使用尾遞歸。有尾遞歸優化的編譯器會把尾遞歸編譯成循環的形式,若是沒有尾遞歸優化,那就本身寫成循環的形式。以下面的例子所示:函數式編程
//尾遞歸函數,返回一個函數調用,而且這個函數調用是本身 function haha(a, b) { if(b) return b; return haha(a, a-1); } //優化成循環的形式 function yaya(a) { let b = a; while(b) { b = b - 1; } }
須要注意的是,看上面尾遞歸的代碼,有一點很重要,就是用一個b變量來存上一次遞歸的值。這是尾遞歸經常使用的方法。另外,其實上面尾遞歸的代碼不須要變量b,但爲了便於說明,因此我加上了變量b。函數
可是關於遞歸還有一種形式,就是相互遞歸,以下面的代碼所示:學習
function haha1(a) { if(!a) return a; return haha2(a-1); } function haha2(a) { if(!a) return a; return haha1(a-1); } haha1(100); //輸出0 haha1(12345678); //輸出Maximum call stack size exceeded
能夠看到,當相互遞歸層數過多的時候,也會發生棧溢出的狀況。優化
蹦牀函數就是解決上面問題的方法。es5
首先咱們改寫上面的相互遞歸函數:
function haha1(a) { if(!a) return a; return function() { return haha2(a-1); } } function haha2(a) { if(!a) return a; return function() { return haha1(a-1); } }
這個改寫就是創建一個閉包來封裝相互遞歸的函數,它的好處是因爲不是直接的相互遞歸調用,因此不會把上一次的遞歸做用域推動棧中,而是把封裝函數儲存在堆裏面,利用堆這個容量更大但讀取時間更慢的儲存形式來替代棧這個容量小但讀取時間快的儲存形式,用時間來換取空間。
咱們嘗試使用一下上面的函數:
haha1(3)(); //輸出一個函數 haha1(3)()()(); //輸出0
經過上面的示例能夠看到,若是參數不是3而是很大的一個數字的時候,咱們就須要寫不少個括號來實現調用不少次。爲了簡便,咱們能夠把這種調用形式寫成函數,這就是蹦牀函數。以下所示:
function trampoline(func, a) { let result = func.call(func, a); while(typeof result === 'function') { result = result(); } return result; }
基本原理是一直調用,直到返回值不是一個函數爲止。
最後來使用蹦牀函數:
trampoline(haha1, 12345678); //過一下子就輸出0
因爲儲存在堆中,因此耗時較長,過了一下子纔會輸出0,可是並無報棧溢出的錯誤。