成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

[翻譯] JS的遞歸與TCO尾調(diào)用優(yōu)化

pekonchan / 819人閱讀

這兩天搜了下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的值返回一個sumsum的參數(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被返回出來。

注意:以上主要還是根據(jù)我自己的理解來闡述邏輯, 確實比較繞,作者原文寫得更加詳細(xì)

總結(jié)

以上方法就是在不改動源碼的情況下實現(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

相關(guān)文章

  • 翻譯連載 | 第 9 章:遞歸(下)-《JavaScript輕量級函數(shù)式編程》 |《你不知道JS

    摘要:每個函數(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...

    LeviDing 評論0 收藏0
  • JS遞歸二叉樹遍歷

    摘要:貌似大部分語言中的遞歸都差不多,之所以在標(biāo)題加是因為搜了下后感覺網(wǎng)上用來描述這概念的不多,簡單地說遞歸就是函數(shù)調(diào)用自己的過程。 貌似大部分語言中的遞歸都差不多, 之所以在標(biāo)題加JS是因為搜了下后感覺網(wǎng)上用js來描述這概念的不多, 簡單地說遞歸就是函數(shù)調(diào)用自己的過程。下面的栗子可以很直觀地展示遞歸的執(zhí)行過程: function rec(x){ if(x!==1){ console....

    church 評論0 收藏0
  • 前端筆記(四) ES6常用語法

    摘要:函數(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]...

    church 評論0 收藏0
  • JavaScript中遞歸

    摘要:第三次第四次設(shè)想,如果傳入的參數(shù)值特別大,那么這個調(diào)用棧將會非常之大,最終可能超出調(diào)用棧的緩存大小而崩潰導(dǎo)致程序執(zhí)行失敗。注意尾遞歸不一定會將你的代碼執(zhí)行速度提高相反,可能會變慢。 譯者按: 程序員應(yīng)該知道遞歸,但是你真的知道是怎么回事么? 原文: All About Recursion, PTC, TCO and STC in JavaScript 譯者: Fundebug ...

    Jacendfeng 評論0 收藏0
  • js 實現(xiàn)斐波那契數(shù)列(數(shù)組緩存、動態(tài)規(guī)劃、調(diào)用優(yōu)化)

    摘要:根據(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 ...

    趙連江 評論0 收藏0

發(fā)表評論

0條評論

pekonchan

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<