遞歸優化
遞歸在我們平時擼碼中會經常用到,不過可能很多人不知道遞歸的弊端,就是會導致調用棧越來越深。如果沒有節制的使用遞歸可能會導致調用棧溢出。
- 那什麼是遞歸呢?
遞歸調用是一種特殊的嵌套調用,是某個函數調用自己或者是調用其他函數後再次調用自己的,只要函數之間互相調用能產生循環的則一定是遞歸調用,遞歸調用一種解決方案,一種是邏輯思想,將一個大工作分爲逐漸減小的小工作,比如說一個和尚要搬50塊石頭,他想,只要先搬走49塊,那剩下的一塊就能搬完了,然後考慮那49塊,只要先搬走48塊,那剩下的一塊就能搬完了,遞歸是一種思想,只不過在程序中,就是依靠函數嵌套這個特性來實現了。 - 那什麼又是調用棧呢?
下面的是我寫的一個簡單的遞歸調用,通過斷點我們可以看到每執行一個test函數,調用棧就會多一個test函數。
當我們執行到i=0的時候,這個時候調用棧是最深的有11個test函數,之後又會逐個移除test函數,可以看圖二的動圖,可以看出調用棧是先進後出的
function test (i) {
if (i < 0) return
test(--i)
}
// 這個會調用自身11次
test(10)
圖1:
圖2:
那怎麼對遞歸進行優化呢,既能起到遞歸的作用又不會加深調用棧
這裏會用到while循環的思想,調用棧之所以會加深主要是因爲方法內調用方法,必須等待方法執行完成這個任務纔算是真正的結束,就像A同學有個任務1,這個任務是讓B同學完成任務2,在B同學沒有完成之前,A同學一直處理工作狀態。
那while循環是什麼原理呢,可以理解爲將有調用關係的方法平鋪爲同一級別。這需要引入額外的方法來做調度,本來test方法需要調用自己10次的,現在用方法b通過標記的方法來決定是否需要調用test方法
- 下面的的例子就是實現遞歸優化的實現方法(這裏複製於阮一峯的es6教程)
- 這是一個很巧妙的方法,我說下它的實現步驟:
- 利用閉包將f方法保留(這裏的f方法就是我們需要遞歸調用的方法)
- 創建value、active、accumulated三個變量,並利用了閉包原理避免被垃圾回收
- accumulated是保存每次f方法調用後需要傳入f的新的形參,active是標記f方法是否執行到了最後一次循環,value是記錄需要返回的值
- 下面的因爲tco會return一個新的函數accumulator,所以sum=accumulator,然後再accumulator內只要accumulated長度不爲0,while就會一直執行,每次執行sum方法就會accumulated.push(arguments)方法,這樣accumulated長度就不會爲0。所以只要f.apply(this, accumulated.shift())執行的時候一旦不調用sum(x + 1, y - 1)方法,accumulated就不會有push操作,這時while就會停止。然後就是active,我們看到if (!active) {...}這個操作,這裏保證了只有第一次調用accumulator方法時會進入while循環,剩下的只是起到accumulated.push(arguments)的作用。直到while循環停止,return出來的就是經過n次調用f方法後返回的值了。
- 這樣就可以把一個遞歸調用轉換爲while循環實現了
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
// 這裏accumulated將形參入棧
accumulated.push(arguments);
// 這裏保證只有第一次調用纔會進入
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)