這兩天搜了下JS遞歸的相關文章, 覺得這篇文章很不錯, 就順手翻譯了下,也算給自己做個筆記,題目是我自己加的。原文很長,寫得也很詳盡,這里并非逐字翻譯, 而是作者所講的主要概念加上我自己的一些理解,本文中解決方案的實際意義并不是特別大,但算法的邏輯挺有意思,不過也略抽象,理解需要花點時間(囧,估計我太閑了) 文中的用例?全部來自原文:
原文鏈接:(原題為:理解JS函數式編程中的遞歸)
Understanding recursion in functional JavaScript programming
在JS的遞歸調用中,JS引擎將為每次遞歸開辟一段內存用以儲存遞歸截止前的數據,這些內存的數據結構以“棧”的形式存儲,這種方式開銷非常大,并且一般瀏覽器可用的內存非常有限。
下面這個函數使用遞歸的方式求和:
//使用遞歸將求和過程復雜化 function sum(x, y) { if (y > 0) { return sum(x + 1, y - 1); } else { return x; } } sum(1, 10); // => 11
當運算規模較小時,這種方式可以正常輸出結果,可是當把參數變為sum(1,100000)時,就會造成“棧溢出錯誤(stack overflow 這可不是那個問答網站哦)”瀏覽器就會報錯Uncaught RangeError: Maximum call stack size exceeded
尾調用優化 Tail Call Optimisation在有些語言中,執行尾遞歸時將會被自動識別,繼而在運行時優化成循環的形式,這種優化邏輯大多是Tail Call Optimisation尾部調用優化,(尾調用概念就是在函數最后一步調用其他函數,尾遞歸即在最后一步調用自身)關于尾遞歸與尾調優化更詳細的概念解讀可以看下阮一峰的這篇文章? 尾調用優化 (也就是說執行尾遞歸時,程序無須儲存之前調用棧的值,直接在最后一次遞歸中輸出函數運算結果,這樣就大大節省了內存,而這種優化邏輯就是在代碼執行的時候將其轉換為循環的形式)
另外在Babel的說明文檔中也提到了尾調用? BABEL Tail Calls
以上的sum函數, 使用尾遞歸,將是這個樣子:
function sum(x, y) { function recur(a, b) { if (b > 0) { return recur(a + 1, b - 1); } else { return a; } } //尾遞歸即在程序尾部調用自身,注意這里沒有其他的運算 return recur(x, y); } sum(1, 10); // => 11
以上這種寫法在有TCO機制的語言中將在執行時內部優化成循環形式而不會產生“棧溢出”錯誤,注意,在當前版本的JS中以上寫法是無效的!因為在當前普遍的JS版本(ES5)中并沒有這個優化機制。但是在ES6中已經實現了這個機制 在當前普遍的JS版本中我們只能使用替代方案。
替代方案這里插一句:使用Babel可以在當前JS版本中用ES6的特性(Babel可以將使用ES6特性編程的代碼轉換成兼容的ES5形式),將原sum()函數輸入Babel的編譯器后,確實被轉換成了循環的形式,感興趣的同學可以自己試試:
BABEL編譯器轉換sum()函數的結果如下(對于算法邏輯不太感興趣的同學看到這里就差不多了,
可以直接將一些深遞歸放到Babel中轉換下就可以了):var _again = true; _function: while (_again) { var x = _x, y = _x2; _again = false; if (y > 0) { _x = x + 1; _x2 = y - 1; _again = true; continue _function; } else { return x; } } }
在當前的JS版本(ES5)中可以使用以下方式來優化遞歸。
我們可以定義一個Trampolining(蹦床)函數來解決參數過大造成的“棧溢出”問題。
//放入trampoline中的函數將被轉換為函數的輸出結果 function trampoline(f) { while (f && f instanceof Function) { f = f(); } return f; } function sum(x, y) { function recur(x, y) { if (y > 0) { return recur.bind(null, x + 1, y - 1); } else { return x; } } // return trampoline(recur.bind(null, x, y)); } sum(1, 10); // => 11
在以上的方案中, trampoline函數接受一個函數作為參數,如果參數是函數就被執行后返回,如果參數不是函數將被直接返回,嵌套函數recur中,當y>0時返回一個參數更新了的函數,這個函數被轉入trampoline中循環,直到recur返回x,x不是函數于是在trampoline中被直接返回。原文中作者對于每一步都有詳盡的解釋, 感興趣的同學建議可以去看看原文。簡單地說:以上邏輯就是將遞歸變成一個條件, 而外層trampoline函數執行這個條件判斷并循環。好吧,接下來更繞的來了-_-#
以上這種方法雖然解決了大參數遞歸的問題,但是卻需要將代碼轉換成trampoline的模式,比較不靈活, 下面作者介紹了一種更靈活方便的方案。
更好的方案作者在此警告:前方高能, 該方法不需要改動源碼,但是略抽象,理解可能需要花點時間。
function tco(f) { var value; var active = false; var accumulated = []; return function accumulator() { accumulated.push(arguments); if (!active) { active = true; while (accumulated.length) { value = f.apply(this, accumulated.shift()); } active = false; return value; } } } //這種方式確實有點奇怪,但的確沒有改動很多源碼,只是以直接量的形式使用tco函數包裹源碼 var sum = tco(function(x, y) { if (y > 0) { return sum(x + 1, y - 1) } else { return x } }); sum(1, 10) // => 11 sum(1, 100000) // => 100001 沒有造成棧溢出
首先以函數表達式的形式將tco函數的返回值賦給sum,tco函數的返回值是accumulator函數,也就是說當執行sum(1,10)的時候即是在執行accumulator(1,10),牢記這點對后續理解很有幫助。
accumulator是個閉包,這意味著可以訪問在tco中定義的value,active以及accumulated
前面已經講了,當我們執行sum的時候相當于是執行accumulator,于是accumulator 將實參傳入accumulated數組,比如執行sum(1,10)那么這里傳入的就是類數組對象[1,10],accumulated現在就是一個length為1的二維數組。
進入while循環,這里是關鍵:value = f.apply(this, accumulated.shift()); 在這條語句中, f表示外包的匿名函數,它判斷y的值后返回一個sum (這里很容易產生混亂,如果我們忽略while循環中的細節,很容易將其誤認為也是遞歸)
匿名函數f判斷y的值返回一個sum,sum的參數被改變了,前面提到執行sum相當于執行accumulator,于是新的參數被加入到了accumulator但是因為這時active的值依然是true(因為現在執行流還在while循環里),所以執行這個被返回的sum就會得到一個undefined的值,value被賦值為undefined
可是因為執行了被返回的sum(也就是執行了accumulator)盡管沒有進入if(!active),但是執行了第一條語句,所以accumulated被重新push進了在外包的匿名函數中被修改的實參,所以while循環繼續(理解這里是關鍵)。
while循環一直執行到accumulated中的值為空, 在value = f.apply(this, accumulated.shift()); 每次return一次sum后accumulated 都會被重新推入一個實參(accumulated的length始終為1),直到匿名的外包函數return出x,于是x的值被賦給value被返回出來。
總結注意:以上主要還是根據我自己的理解來闡述邏輯, 確實比較繞,作者原文寫得更加詳細
以上方法就是在不改動源碼的情況下實現的TCO優化, 作者在該文章的Update中介紹了另外的非TCO的優化遞歸的方法,不過篇幅有限就不再貼出來了,就我自身感覺而言,如果對算法的邏輯實現不感興趣, 大可以直接用Babel將深遞歸轉換成優化后的形式。另外這也有一篇介紹JS中遞歸與循環的的文章,其中也有TCO優化的相關介紹:
?Recursion in Functional JavaScript
感覺以上代碼的實際意義可能并沒有那么大, 為了寫這篇博客也是耗了我一天,囧rz,但也挺佩服這哥們:“我靠,這也能想得到!”
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86241.html
摘要:每個函數調用都將開辟出一小塊稱為堆棧幀的內存。當第二個函數開始執行,堆棧幀增加到個。當這個函數調用結束后,它的幀會從堆棧中退出。保持堆棧幀跟蹤函數調用的狀態,并將其分派給下一個遞歸調用迭。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關于譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTM...
摘要:貌似大部分語言中的遞歸都差不多,之所以在標題加是因為搜了下后感覺網上用來描述這概念的不多,簡單地說遞歸就是函數調用自己的過程。 貌似大部分語言中的遞歸都差不多, 之所以在標題加JS是因為搜了下后感覺網上用js來描述這概念的不多, 簡單地說遞歸就是函數調用自己的過程。下面的栗子可以很直觀地展示遞歸的執行過程: function rec(x){ if(x!==1){ console....
摘要:函數更好的尾遞歸優化實現傳入類數組對象且每次的值在改變。尾遞歸實現改寫一般的遞歸函數確保最后一步只調用自身。返回一個遍歷器對象用循環遍歷。和循環它是一種遍歷器接口為各種不同的數據結構提供統一的訪問機制。 解構賦值 //數組的解構賦值 let [a, b, c] = [1, 2, 3]; a // 1 b // 2 c // 3 let [a, [[b], c]] = [1, [[2]...
摘要:第三次第四次設想,如果傳入的參數值特別大,那么這個調用棧將會非常之大,最終可能超出調用棧的緩存大小而崩潰導致程序執行失敗。注意尾遞歸不一定會將你的代碼執行速度提高相反,可能會變慢。 譯者按: 程序員應該知道遞歸,但是你真的知道是怎么回事么? 原文: All About Recursion, PTC, TCO and STC in JavaScript 譯者: Fundebug ...
摘要:根據該規則,返回第個斐波那契數。尾遞歸函數調用自身,稱為遞歸。一個前端眼中的斐波那契數列解斐波那契數列的實用解法調用棧尾遞歸和手動優化尾調用優化譯我從用寫斐波那契生成器中學到的令人驚訝的件事 斐波那契數列是以下一系列數字: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, ... 在種子數字 0 和 1 ...
閱讀 3338·2023-04-26 03:05
閱讀 1458·2019-08-30 13:09
閱讀 1907·2019-08-30 13:05
閱讀 886·2019-08-29 12:42
閱讀 1385·2019-08-28 18:18
閱讀 3446·2019-08-28 18:09
閱讀 512·2019-08-28 18:00
閱讀 1712·2019-08-26 12:10