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