摘要:每個函數(shù)調(diào)用都將開辟出一小塊稱為堆棧幀的內(nèi)存。當?shù)诙€函數(shù)開始執(zhí)行,堆棧幀增加到個。當這個函數(shù)調(diào)用結(jié)束后,它的幀會從堆棧中退出。保持堆棧幀跟蹤函數(shù)調(diào)用的狀態(tài),并將其分派給下一個遞歸調(diào)用迭。
原文地址:Functional-Light-JS
原文作者:Kyle Simpson-《You-Dont-Know-JS》作者
第 9 章:遞歸(下) 棧、堆關(guān)于譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTML 最堅實的梁柱;分享,是 CSS 里最閃耀的一瞥;總結(jié),是 JavaScript 中最嚴謹?shù)倪壿?。?jīng)過捶打磨練,成就了本書的中文版。本書包含了函數(shù)式編程之精髓,希望可以幫助大家在學習函數(shù)式編程的道路上走的更順暢。比心。
譯者團隊(排名不分先后):阿希、blueken、brucecham、cfanlife、dail、kyoko-df、l3ve、lilins、LittlePineapple、MatildaJin、冬青、pobusama、Cherry、蘿卜、vavd317、vivaxy、萌萌、zhouyao
一起看下之前的兩個遞歸函數(shù) isOdd(..) 和 isEven(..):
function isOdd(v) { if (v === 0) return false; return isEven( Math.abs( v ) - 1 ); } function isEven(v) { if (v === 0) return true; return isOdd( Math.abs( v ) - 1 ); }
如果你執(zhí)行下面這行代碼,在大多數(shù)瀏覽器里面都會報錯:
isOdd( 33333 ); // RangeError: Maximum call stack size exceeded
這個錯誤是什么情況?引擎拋出這個錯誤,是因為它試圖保護系統(tǒng)內(nèi)存不會被你的程序耗盡。為了解釋這個問題,我們需要先看看當函數(shù)調(diào)用時JS引擎中發(fā)生了什么。
每個函數(shù)調(diào)用都將開辟出一小塊稱為堆棧幀的內(nèi)存。堆棧幀中包含了函數(shù)語句當前狀態(tài)的某些重要信息,包括任意變量的值。之所以這樣,是因為一個函數(shù)暫停去執(zhí)行另外一個函數(shù),而另外一個函數(shù)運行結(jié)束后,引擎需要返回到之前暫停時候的狀態(tài)繼續(xù)執(zhí)行。
當?shù)诙€函數(shù)開始執(zhí)行,堆棧幀增加到 2 個。如果第二個函數(shù)又調(diào)用了另外一個函數(shù),堆棧幀將增加到 3 個,以此類推?!皸!钡囊馑际?,函數(shù)被它前一個函數(shù)調(diào)用時,這個函數(shù)幀會被“推”到最頂部。當這個函數(shù)調(diào)用結(jié)束后,它的幀會從堆棧中退出。
看下這段程序:
function foo() { var z = "foo!"; } function bar() { var y = "bar!"; foo(); } function baz() { var x = "baz!"; bar(); } baz();
來一步步想象下這個程序的堆棧幀:
注意: 如果這些函數(shù)間沒有相互調(diào)用,而只是依次執(zhí)行 -- 比如前一個函數(shù)運行結(jié)束后才開始調(diào)用下一個函數(shù) baz(); bar(); foo(); -- 則堆棧幀并沒有產(chǎn)生;因為在下一個函數(shù)開始之前,上一個函數(shù)運行結(jié)束并把它的幀從堆棧里面移除了。
所以,每一個函數(shù)運行時候,都會占用一些內(nèi)存。對多數(shù)程序來說,這沒什么大不了的,不是嗎?但是,一旦你引用了遞歸,問題就不一樣了。
雖然你幾乎肯定不會在一個調(diào)用棧中手動調(diào)用成千(或數(shù)百)次不同的函數(shù),但你很容易看到產(chǎn)生數(shù)萬個或更多遞歸調(diào)用的堆棧。
當引擎認為調(diào)用棧增加的太多并且應該停止增加時候,它會以主觀的限制來阻止當前步驟,所以 isOdd(..) 或 isEven(..) 函數(shù)拋出了 RangeError 未知錯誤。這不太可能是內(nèi)存接近零時候產(chǎn)生的限制,而是引擎的預測,因為如果這種程序持續(xù)運行下去,內(nèi)存會爆掉的。由于引擎無法判斷一個程序最終是否會停止,所以它必須做出確定的猜測。
引擎的限制因情況而定。規(guī)范里面并沒有任何說明,因此,它也不是 必需的。但如果沒有限制的話,設(shè)備很容易遭到破壞或惡意代碼攻擊,故而幾乎所有的JS引擎都有一個限制。不同的設(shè)備環(huán)境、不同的引擎,會有不同的限制,也就無法預測或保證函數(shù)調(diào)用棧能調(diào)用多少次。
在處理大數(shù)據(jù)量時候,這個限制對于開發(fā)人員來說,會對遞歸的性能有一定的要求。我認為,這種限制也可能是造成開發(fā)人員不喜歡使用遞歸編程的最大原因。
遺憾的是,遞歸編程是一種編程思想而不是主流的編程技術(shù)。
遞歸編程和內(nèi)存限制都要比 JS 技術(shù)出現(xiàn)的早。追溯到上世紀 60 年代,當時開發(fā)人員想使用遞歸編程并希望運行在他們強大的計算機的設(shè)備,而所謂強大計算機的內(nèi)存,尚遠不如我們今天在手表上的內(nèi)存。
幸運的是,在那個希望的原野上,進行了一個有力的觀測。該技術(shù)稱為 尾調(diào)用。
它的思路是如果一個回調(diào)從函數(shù) baz() 轉(zhuǎn)到函數(shù) bar() 時候,而回調(diào)是在函數(shù) baz() 的最底部執(zhí)行 -- 也就是尾調(diào)用 -- 那么 baz() 的堆棧幀就不再需要了。也就意謂著,內(nèi)存可以被回收,或只需簡單的執(zhí)行 bar() 函數(shù)。 如圖所示:
尾調(diào)用并不是遞歸特有的;它適用于任何函數(shù)調(diào)用。但是,在大多數(shù)情況下,你的手動非遞歸調(diào)用棧不太可能超過 10 級,因此尾調(diào)用對你程序內(nèi)存的影響可能相當?shù)汀?/p>
在遞歸的情況下,尾調(diào)用作用很明顯,因為這意味著遞歸堆??梢浴坝肋h”運行下去,唯一的性能問題就是計算,而不再是固定的內(nèi)存限制。在固定的內(nèi)存中尾遞歸可以運行 O(1) (常數(shù)階時間復雜度計算)。
這些技術(shù)通常被稱為尾調(diào)用優(yōu)化(TCO),但重點在于從優(yōu)化技術(shù)中,區(qū)分出在固定內(nèi)存空間中檢測尾調(diào)用運行的能力。從技術(shù)上講,尾調(diào)用并不像大多數(shù)人所想的那樣,它們的運行速度可能比普通回調(diào)還慢。TCO 是關(guān)于把尾調(diào)用更加高效運行的一些優(yōu)化技術(shù)。
正確的尾調(diào)用 (PTC)在 ES6 出來之前,JavaScript 對尾調(diào)用一直沒明確規(guī)定(也沒有禁用)。ES6 明確規(guī)定了 PTC 的特定形式,在 ES6 中,只要使用尾調(diào)用,就不會發(fā)生棧溢出。實際上這也就意味著,只要正確的使用 PTC,就不會拋出 RangeError 這樣的異常錯誤。
首先,在 JavaScript 中應用 PTC,必須以嚴格模式書寫代碼。如果你以前沒有用過嚴格模式,你得試著用用了。那么,您,應該已經(jīng)在使用嚴格模式了吧???
其次,正確 的尾調(diào)用就像這個樣子:
return foo( .. );
換句話說,函數(shù)調(diào)用應該放在最后一步去執(zhí)行,并且不管返回什么東東,都得有返回( return )。這樣的話,JS 就不再需要當前的堆棧幀了。
下面這些 不能 稱之為 PTC:
foo(); return; // 或 var x = foo( .. ); return x; // 或 return 1 + foo( .. );
注意: 一些 JS 引擎 能夠 把 var x = foo(); return x; 自動識別為 return foo();,這樣也可以達到 PTC 的效果。但這畢竟不符合規(guī)范。
foo(..) 運行結(jié)束之后 1+ 這部分才開始執(zhí)行,所以此時的堆棧幀依然存在。
不過,下面這個 是 PTC:
return x ? foo( .. ) : bar( .. );
x 進行條件判斷之后,或執(zhí)行 foo(..),或執(zhí)行 bar(..),不論執(zhí)行哪個,返回結(jié)果都會被 return 返回掉。這個例子符合 PTC 規(guī)范。
為了避免堆棧增加,PTC 要求所有的遞歸必須是在尾部調(diào)用,因此,二分法遞歸 —— 兩次(或以上)遞歸調(diào)用 —— 是不能實現(xiàn) PTC 的。我們曾在文章的前面部分展示過把二分法遞歸轉(zhuǎn)變?yōu)橄嗷ミf歸的例子。也許我們可以試著化整為零,把多重遞歸拆分成符合 PTC 規(guī)范的單個函數(shù)回調(diào)。
重構(gòu)遞歸如果你想用遞歸來處理問題,卻又超出了 JS 引擎的內(nèi)存堆棧,這時候就需要重構(gòu)下你的遞歸調(diào)用,使它能夠符合 PTC 規(guī)范(或著避免嵌套調(diào)用)。這里有一些重構(gòu)方法也許可以用到,但需要根據(jù)實際情況權(quán)衡。
可讀性強的代碼,是我們的終級目標 —— 謹記,謹記。如果使用遞歸后會造成代碼難以閱讀/理解,那就 不要使用遞歸;換個容易理解的方法吧。
更換堆棧對遞歸來說,最主要的問題是它的內(nèi)存使用情況。保持堆棧幀跟蹤函數(shù)調(diào)用的狀態(tài),并將其分派給下一個遞歸調(diào)用迭。如果我們弄清楚了如何重新排列我們的遞歸,就可以用 PTC 實現(xiàn)遞歸,并利用 JS 引擎對尾調(diào)用的優(yōu)化處理,那么我們就不用在內(nèi)存中保留當前的堆棧幀了。
來回顧下之前用到的一個求和的例子:
function sum(num1,...nums) { if (nums.length == 0) return num1; return num1 + sum( ...nums ); }
這個例子并不符合 PTC 規(guī)范。sum(...nums) 運行結(jié)束之后,num1 與 sum(...nums) 的運行結(jié)果進行了累加。這樣的話,當其余參數(shù) ...nums 再次進行遞歸調(diào)用時候,為了得到其與 num1 累加的結(jié)果,必須要保留上一次遞歸調(diào)用的堆棧幀。
重構(gòu)策略的關(guān)鍵點在于,我們可以通過把 置后 處理累加改為 提前 處理,來消除對堆棧的依賴,然后將該部分結(jié)果作為參數(shù)傳遞到遞歸調(diào)用。換句話說,我們不用在當前運用函數(shù)的堆棧幀中保留 num1 + sum(...num1) 的總和,而是把它傳遞到下一個遞歸的堆棧幀中,這樣就能釋放當前遞歸的堆棧幀。
開始之前,我們做些改動:把部分結(jié)果作為一個新的第一個參數(shù)傳入到函數(shù) sum(..):
function sum(result,num1,...nums) { // .. }
這次我們先把 result 和 num1 提前計算,然后把 result 作為參數(shù)一起傳入:
"use strict"; function sum(result,num1,...nums) { result = result + num1; if (nums.length == 0) return result; return sum( result, ...nums ); }
現(xiàn)在 sum(..) 已經(jīng)符合 PTC 優(yōu)化規(guī)范了!耶!
但是還有一個缺點,我們修改了函數(shù)的參數(shù)傳遞形式后,用法就跟以前不一樣了。調(diào)用者不得不在需要求和的那些參數(shù)的前面,再傳遞一個 0 作為第一個參數(shù)。
sum( /*initialResult=*/0, 3, 1, 17, 94, 8 ); // 123
這就尷尬了。
通常,大家的處理方式是,把這個尷尬的遞歸函數(shù)重新命名,然后定義一個接口函數(shù)把問題隱藏起來:
"use strict"; function sumRec(result,num1,...nums) { result = result + num1; if (nums.length == 0) return result; return sumRec( result, ...nums ); } function sum(...nums) { return sumRec( /*initialResult=*/0, ...nums ); } sum( 3, 1, 17, 94, 8 ); // 123
情況好了些。但依然有問題:之前只需要一個函數(shù)就能解決的事,現(xiàn)在我們用了兩個。有時候你會發(fā)現(xiàn),在處理這類問題上,有些開發(fā)者用內(nèi)部函數(shù)把遞歸 “藏了起來”:
"use strict"; function sum(...nums) { return sumRec( /*initialResult=*/0, ...nums ); function sumRec(result,num1,...nums) { result = result + num1; if (nums.length == 0) return result; return sumRec( result, ...nums ); } } sum( 3, 1, 17, 94, 8 ); // 123
這個方法的缺點是,每次調(diào)用外部函數(shù) sum(..),我們都得重新創(chuàng)建內(nèi)部函數(shù) sumRec(..)。我們可以把他們平級放置在立即執(zhí)行的函數(shù)中,只暴露出我們想要的那個的函數(shù):
"use strict"; var sum = (function IIFE(){ return function sum(...nums) { return sumRec( /*initialResult=*/0, ...nums ); } function sumRec(result,num1,...nums) { result = result + num1; if (nums.length == 0) return result; return sumRec( result, ...nums ); } })(); sum( 3, 1, 17, 94, 8 ); // 123
好啦,現(xiàn)在即符合了 PTC 規(guī)范,又保證了 sum(..) 參數(shù)的整潔性,調(diào)用者不需要了解函數(shù)的內(nèi)部實現(xiàn)細節(jié)。完美!
可是...天吶,本來是簡單的遞歸函數(shù),現(xiàn)在卻出現(xiàn)了很多噪點??勺x性已經(jīng)明顯降低。至少說,這是不成功的。有些時候,這只是我們能做的最好的。
幸運的事,在一些其它的例子中,比如上一個例子,有一個比較好的方式。一起重新看下:
"use strict"; function sum(result,num1,...nums) { result = result + num1; if (nums.length == 0) return result; return sum( result, ...nums ); } sum( /*initialResult=*/0, 3, 1, 17, 94, 8 ); // 123
也許你會注意到,result 就像 num1 一樣,也就是說,我們可以把列表中的第一個數(shù)字作為我們的運行總和;這甚至包括了第一次調(diào)用的情況。我們需要的是重新命名這些參數(shù),使函數(shù)清晰可讀:
"use strict"; function sum(num1,num2,...nums) { num1 = num1 + num2; if (nums.length == 0) return num1; return sum( num1, ...nums ); } sum( 3, 1, 17, 94, 8 ); // 123
帥呆了。比之前好了很多,嗯?!我認為這種模式在聲明/合理和執(zhí)行之間達到了很好的平衡。
讓我們試著重構(gòu)下前面的 maxEven(..)(目前還不符合 PTC 規(guī)范)。就像之前我們把參數(shù)的和作為第一個參數(shù)一樣,我們可以依次減少列表中的數(shù)字,同時一直把遇到的最大偶數(shù)作為第一個參數(shù)。
為了清楚起見,我們可能使用算法策略(類似于我們之前討論過的):
首先對前兩個參數(shù) num1 和 num2 進行對比。
如果 num1 是偶數(shù),并且 num1 大于 num2,num1 保持不變。
如果 num2 是偶數(shù),把 num2 賦值給 num1。
否則的話,num1 等于 undefined。
如果除了這兩個參數(shù)之外,還存在其它參數(shù) nums,把它們與 num1 進行遞歸對比。
最后,不管是什么值,只需返回 num1。
依照上面的步驟,代碼如下:
"use strict"; function maxEven(num1,num2,...nums) { num1 = (num1 % 2 == 0 && !(maxEven( num2 ) > num1)) ? num1 : (num2 % 2 == 0 ? num2 : undefined); return nums.length == 0 ? num1 : maxEven( num1, ...nums ) }
注意: 函數(shù)第一次調(diào)用 maxEven(..) 并不是為了 PTC 優(yōu)化,當它只傳遞 num2 時,只遞歸一級就返回了;它只是一個避免重復 % 邏輯的技巧。因此,只要該調(diào)用是完全不同的函數(shù),就不會增加遞歸堆棧。第二次調(diào)用 maxEven(..) 是基于 PTC 優(yōu)化角度的真正遞歸調(diào)用,因此不會隨著遞歸的進行而造成堆棧的增加。
重申下,此示例僅用于說明將遞歸轉(zhuǎn)化為符合 PTC 規(guī)范以優(yōu)化堆棧(內(nèi)存)使用的方法。求最大偶數(shù)值的更直接方法可能是,先對參數(shù)列表中的 nums 過濾,然后冒泡或排序處理。
基于 PTC 重構(gòu)遞歸,固然對簡單的聲明形式有一些影響,但依然有理由去做這樣的事。不幸的是,存在一些遞歸,即使我們使用了接口函數(shù)來擴展,也不會很好,因此,我們需要有不同的思路。
后繼傳遞格式 (CPS)在 JavaScript 中, continuation 一詞通常用于表示在某個函數(shù)完成后指定需要執(zhí)行的下一個步驟的回調(diào)函數(shù)。組織代碼,使得每個函數(shù)在其結(jié)束時接收另一個執(zhí)行函數(shù),被稱為后繼傳遞格式(CPS)。
有些形式的遞歸,實際上是無法按照純粹的 PTC 規(guī)范重構(gòu)的,特別是相互遞歸。我們之前提到過的 fib(..) 函數(shù),以及我們派生出來的相互遞歸形式。這兩個情況,皆是存在多個遞歸調(diào)用,這些遞歸調(diào)用阻礙了 PTC 內(nèi)存優(yōu)化。
但是,你可以執(zhí)行第一個遞歸調(diào)用,并將后續(xù)遞歸調(diào)用包含在后續(xù)函數(shù)中并傳遞到第一個調(diào)用。盡管這意味著最終需要在堆棧中執(zhí)行更多的函數(shù),但由于后繼函數(shù)所包含的都是 PTC 形式的,所以堆棧內(nèi)存的使用情況不會無限增長。
把 fib(..) 做如下修改:
"use strict"; function fib(n,cont = identity) { if (n <= 1) return cont( n ); return fib( n - 2, n2 => fib( n - 1, n1 => cont( n2 + n1 ) ) ); }
仔細看下都做了哪些事情。首先,我們默認用了第三章中的 cont(..) 后繼函數(shù)表示 identity(..);記住,它只簡單的返回傳遞給它的任何東西。
更重要的是,這里面增加了不僅僅是一個而是兩個后續(xù)函數(shù)。第一個后續(xù)函數(shù)接收 fib(n-2) 的運行結(jié)果作為參數(shù) n2。第二個內(nèi)部后續(xù)函數(shù)接收 fib(n-1)的運行結(jié)果作為參數(shù) n1。當?shù)玫?n1 和 n2 的值后,兩者再相加 (n2 + n1),相加的運行結(jié)果會傳入到下一個后續(xù)函數(shù) cont(..)。
也許這將有助于我們梳理下流程:就像我們之前討論的,在遞歸堆棧之后,當我們傳遞部分結(jié)果而不是返回它們時,每一步都被包含在一個后續(xù)函數(shù)中,這拖慢了計算速度。這個技巧允許我們執(zhí)行多個符合 PTC 規(guī)范的步驟。
在靜態(tài)語言中,CPS通常為尾調(diào)用提供了編譯器可以自動識別并重新排列遞歸代碼以利用的機會。很可惜,不能用在原生 JS 上。
在 JavaScript 中,你得自己書寫出符合 CPS 格式的代碼。這并不是明智的做法;以命令符號聲明的形式肯定會讓內(nèi)容有些不清楚。 但總的來說,這種形式仍然要比 for 循環(huán)更具有聲明性。
警告: 我們需要注意的一個比較重要的事項是,在 CPS 中,創(chuàng)建額外的內(nèi)部后續(xù)函數(shù)仍然消耗內(nèi)存,但有些不同。并不是之前的堆棧幀累積,閉包只是消耗多余的內(nèi)存空間(一般情況下,是堆棧里面的多余內(nèi)存空間)。在這些情況下,引擎似乎沒有啟動 RangeError 限制,但這并不意味著你的內(nèi)存使用量是按比例固定好的。
彈簧床除了 CPS 后續(xù)傳遞格式之外,另外一種內(nèi)存優(yōu)化的技術(shù)稱為彈簧床。在彈簧床格式的代碼中,同樣的創(chuàng)建了類似 CPS 的后續(xù)函數(shù),不同的是,它們沒有被傳遞,而是被簡單的返回了。
不再是函數(shù)調(diào)用另外的函數(shù),堆棧的深度也不會大于一層,因為每個函數(shù)只會返回下一個將調(diào)用的函數(shù)。循環(huán)只是繼續(xù)運行每個返回的函數(shù),直到再也沒有函數(shù)可運行。
彈簧床的優(yōu)點之一是在非 PTC 環(huán)境下你一樣可以應用此技術(shù)。另一個優(yōu)點是每個函數(shù)都是正常調(diào)用,而不是 PTC 優(yōu)化,所以它可以運行得更快。
一起來試下 trampoline(..):
function trampoline(fn) { return function trampolined(...args) { var result = fn( ...args ); while (typeof result == "function") { result = result(); } return result; }; }
當返回一個函數(shù)時,循環(huán)繼續(xù),執(zhí)行該函數(shù)并返回其運行結(jié)果,然后檢查返回結(jié)果的類型。一旦返回的結(jié)果類型不是函數(shù),彈簧床就認為函數(shù)調(diào)用完成了并返回結(jié)果值。
所以我們可能需要使用前面講到的,將部分結(jié)果作為參數(shù)傳遞的技巧。以下是我們在之前的數(shù)組求和中使用此技巧的示例:
var sum = trampoline( function sum(num1,num2,...nums) { num1 = num1 + num2; if (nums.length == 0) return num1; return () => sum( num1, ...nums ); } ); var xs = []; for (let i=0; i<20000; i++) { xs.push( i ); } sum( ...xs ); // 199990000
缺點是你需要將遞歸函數(shù)包裹在執(zhí)行彈簧床功能的函數(shù)中; 此外,就像 CPS 一樣,需要為每個后續(xù)函數(shù)創(chuàng)建閉包。然而,與 CPS 不一樣的地方是,每個返回的后續(xù)數(shù)數(shù),運行并立即完成,所以,當調(diào)用堆棧的深度用盡時,引擎中不會累積越來越多的閉包。
除了執(zhí)行和記憶性能之外,彈簧床技術(shù)優(yōu)于CPS的優(yōu)點是它們在聲明遞歸形式上的侵入性更小,由于你不必為了接收后續(xù)函數(shù)的參數(shù)而更改函數(shù)參數(shù),所以除了執(zhí)行和內(nèi)存性能之外,彈簧床技術(shù)優(yōu)于 CPS 的地方還有,它們在聲明遞歸形式上侵入性更小。雖然彈簧床技術(shù)并不是理想的,但它們可以有效地在命令循環(huán)代碼和聲明性遞歸之間達到平衡。
總結(jié)遞歸,是指函數(shù)遞歸調(diào)用自身。呃,這就是遞歸的定義。明白了吧!?
直遞歸是指對自身至少調(diào)用一次,直到滿足基本條件才能停止調(diào)用。多重遞歸(像二分遞歸)是指對自身進行多次調(diào)用。相互遞歸是當兩個或以上函數(shù)循環(huán)遞歸 相互 調(diào)用。而遞歸的優(yōu)點是它更具聲明性,因此通常更易于閱讀。
遞歸的優(yōu)點是它更具聲明性,因此通常更易于閱讀。缺點通常是性能方面,但是相比執(zhí)行速度,更多的限制在于內(nèi)存方面。
尾調(diào)用是通過減少或釋放堆棧幀來節(jié)約內(nèi)存空間。要在 JavaScript 中實現(xiàn)尾調(diào)用 “優(yōu)化”,需要基于嚴格模式和適當?shù)奈舱{(diào)用( PTC )。我們也可以混合幾種技術(shù)來將非 PTC 遞歸函數(shù)重構(gòu)為 PTC 格式,或者至少能通過平鋪堆棧來節(jié)約內(nèi)存空間。
謹記:遞歸應該使代碼更容易讀懂。如果你誤用或濫用遞歸,代碼的可讀性將會比命令形式更糟。千萬不要這樣做。
【上一章】翻譯連載 | 第 9 章:遞歸(上)-《JavaScript輕量級函數(shù)式編程》 |《你不知道的JS》姊妹篇
iKcamp原創(chuàng)新書《移動Web前端高效開發(fā)實戰(zhàn)》已在亞馬遜、京東、當當開售。
滬江Web前端上海團隊招聘【W(wǎng)eb前端架構(gòu)師】,有意者簡歷至:[email protected]
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/88665.html
摘要:一旦我們滿足了基本條件值為,我們將不再調(diào)用遞歸函數(shù),只是有效地執(zhí)行了。遞歸深諳函數(shù)式編程之精髓,最被廣泛引證的原因是,在調(diào)用棧中,遞歸把大部分顯式狀態(tài)跟蹤換為了隱式狀態(tài)。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTML 最堅實的梁柱;...
摘要:我稱之為輕量級函數(shù)式編程。序眾所周知,我是一個函數(shù)式編程迷。函數(shù)式編程有很多種定義。本書是你開啟函數(shù)式編程旅途的絕佳起點。事實上,已經(jīng)有很多從頭到尾正確的方式介紹函數(shù)式編程的書了。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson?。 禮ou-Dont-Know-JS》作者 譯者團隊(排名不分先后):阿希、blueken、brucecham、...
摘要:把數(shù)據(jù)的流向想象成糖果工廠的一條傳送帶,每一次操作其實都是冷卻切割包裝糖果中的一步。在該章節(jié)中,我們將會用糖果工廠的類比來解釋什么是組合。糖果工廠靠這套流程運營的很成功,但是和所有的商業(yè)公司一樣,管理者們需要不停的尋找增長點。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個流淌...
摘要:這就是積極的函數(shù)式編程。上一章翻譯連載第章遞歸下輕量級函數(shù)式編程你不知道的姊妹篇原創(chuàng)新書移動前端高效開發(fā)實戰(zhàn)已在亞馬遜京東當當開售。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTML 最堅實的梁柱;分享,是 CSS 里最閃耀的一瞥;總...
摘要:所以我覺得函數(shù)式編程領(lǐng)域更像學者的領(lǐng)域。函數(shù)式編程的原則是完善的,經(jīng)過了深入的研究和審查,并且可以被驗證。函數(shù)式編程是編寫可讀代碼的最有效工具之一可能還有其他。我知道很多函數(shù)式編程編程者會認為形式主義本身有助于學習。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson?。 禮ou-Dont-Know-JS》作者 關(guān)于譯者:這是一個流淌著滬江血液...
閱讀 4426·2021-11-19 09:59
閱讀 3344·2021-10-12 10:12
閱讀 2649·2021-09-22 15:25
閱讀 3352·2019-08-30 15:55
閱讀 1198·2019-08-29 11:27
閱讀 1478·2019-08-28 18:06
閱讀 2752·2019-08-26 13:41
閱讀 2567·2019-08-26 13:41