概述
這是我在學習函數式編程的時候,關於遞歸,尾遞歸,相互遞歸和蹦床函數的一些心得,記下來供以后開發時參考,相信對其他人也有用。
參考資料:JavaScript玩轉Clojure大法之 - Trampoline
遞歸
我們知道,es5是沒有尾遞歸優化的,所以在遞歸的時候,如果層數太多,就會報“Maximum call stack size exceeded”的錯誤。就連下面這個及其簡單的遞歸函數都會報“Maximum call stack size exceeded”的錯誤。
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”的錯誤?我覺得原因是在每次遞歸調用的時候,會把當前作用域里面的基本類型的值推進棧中,所以一旦遞歸層數過多,棧就會溢出,所以會報錯。
注意:
- js中的棧只會儲存基本類型的值,比如:number, string, undefined, null, boolean。
- 為什么在調用下一層遞歸函數的時候沒有釋放上一層遞歸函數的作用域?因為在回來的時候還需要用到里面的變量。
尾遞歸
怎么優化上面的情況呢?方法是使用尾遞歸。有尾遞歸優化的編譯器會把尾遞歸編譯成循環的形式,如果沒有尾遞歸優化,那就自己寫成循環的形式。如下面的例子所示:
//尾遞歸函數,返回一個函數調用,並且這個函數調用是自己
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
可以看到,當相互遞歸層數過多的時候,也會發生棧溢出的情況。
蹦床函數
蹦床函數就是解決上面問題的方法。
首先我們改寫上面的相互遞歸函數:
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,但是並沒有報棧溢出的錯誤。