函數(shù)式編程指南

1. 什么是函數(shù)式編程

1.1

當(dāng)考慮應(yīng)用設(shè)計時樱调,我們應(yīng)該問問自己是否遵從了以下的設(shè)計原則
? 可擴(kuò)展性一一我是否需要不斷地重構(gòu)代碼來支持額外的功能?
? 易模塊化一一如果我更改了一個文件,另一個文件會不會受到影響?
? 可重用性一一是否有很多重復(fù)的代碼?
? 可測性一一給這些函數(shù)添加單元測試是否讓我糾結(jié)?
? 易推理性一一我寫的代碼是否非結(jié)構(gòu)化嚴(yán)重并難以推理?

1.2

函數(shù)式編程需要你在思考解決問題的思路時有所變化妥凳。其實使用函數(shù)來獲得結(jié)果并不重要,函數(shù)式編程的目標(biāo)是使用函數(shù)來抽象作用在數(shù)據(jù)之上的控制流與操作答捕,從而在系統(tǒng)中消除副作用并減少對狀態(tài)的改變逝钥。
來更簡潔的描述一下:函數(shù)式編程是指為創(chuàng)建不可變的程序,通過消除外部可見的副作用拱镐,來對純函數(shù)的聲明式的求值過程艘款。

1.3 我們首先來了解一下它所基于的基本概念

? 聲明式編程:將程序的描述與求值分離開來。它關(guān)注于如何用各種表達(dá)式來描述程序邏輯沃琅,而不一定要指明其控制流或狀態(tài)的變化
? 純函數(shù)

  1. 僅取決于提供的輸入哗咆,而不依賴于任何在函數(shù)求值期間或調(diào)用間隔時可能變化
    的隱藏狀態(tài)和外部狀態(tài) 。
  2. 不會造成超出其作用域的變化阵难,例如修改全局對象或引用傳遞的參數(shù) 。
    ? 引用透明:相同的輸入總是產(chǎn)生相同的輸出
    ? 不可變性

1.4 優(yōu)點

? 促使將任務(wù)分解成簡單的函數(shù):1. 單一職責(zé) 2. 組合
? 使用流式的調(diào)用鏈來處理數(shù)據(jù):函數(shù)鏈?zhǔn)且环N惰性計算程序芒填,這意味著當(dāng)需要時才會執(zhí)行呜叫。有效地模擬了其他函數(shù)式語言的按需調(diào)用的行為。
? 通過響應(yīng)式范式降低事件驅(qū)動代碼的復(fù)雜性: observable

2. 高階函數(shù)

鑒于函數(shù)的行為與普通對象類似殿衰,其理所當(dāng)然地可以作為其他函數(shù)的參數(shù)進(jìn)行傳遞朱庆,或是由其他函數(shù)返回。這些函數(shù)則稱為高階函數(shù)闷祥。
因為函數(shù)的一等性和高階性娱颊, JavaScript函數(shù)具有的行為傲诵,也就是說,函數(shù)就是一個基于輸人的且尚未求值的不可變的值箱硕。

3. 函數(shù)調(diào)用的方法

? 作為全局函數(shù)一一 其中 this 的引用可以是 global 對象或是 undefined (在嚴(yán)格模式中)
? 作為方法 一一其中 this 的引用是方法的所有者拴竹,這是Javascript 的面向?qū)ο筇匦缘闹匾糠?br> ? 作為構(gòu)造函數(shù)與 new 一起使用 一一 這種方式會返回新創(chuàng)建對象的引用

4. 閉包

4.1 概念

閉包是一種能夠在函數(shù)聲明過程中將環(huán)境信息與所屬函數(shù)綁定在一起的數(shù)據(jù)結(jié)構(gòu)。 它是基于函數(shù)聲明的文本位置的剧罩,因此也被稱為圍繞函數(shù)定義的靜態(tài)作用域或詞法作用域 栓拜。閉包能夠使函數(shù)訪問其環(huán)境狀態(tài),使得代碼更清晰可讀惠昔。你很快就會看到幕与,閉包不僅應(yīng)用于函數(shù)式編程的高階函數(shù)中,也可用于事件處理和回調(diào)镇防、模擬私有成員變量啦鸣,還能用于彌補(bǔ)一些 JavaScript 的不足。

支配函數(shù)閉包行為的規(guī)則與 JavaScript 的作用域規(guī)則密切相關(guān)来氧。作用域能夠?qū)⒁唤M變量綁定诫给,并定義變量定義的代碼段。從本質(zhì)上講饲漾,閉包就是函數(shù)繼承而來的作用域蝙搔, 這類似于對象方法是如何訪問其繼承的實例變量的,它們都具有其父類型的引用考传。在內(nèi)嵌函數(shù)中能夠很清楚地看到閉包吃型。

函數(shù)的閉包包括以下內(nèi)容:
? 函數(shù)的所有參數(shù)
? 外部作用域的所有變量(當(dāng)然也包括所有的全局變量)

4.2 函數(shù)作用域

  1. 首先檢查變量的函數(shù)作用域。
  2. 如果不是在局部作用域內(nèi)僚楞,那么逐層向外檢查各詞法作用域勤晚,搜索該變量的引用,直到全局作用域泉褐。
  3. 如果無法找到變量引用赐写,那么 JavaScript 將返回 undefined。

4.3 實際應(yīng)用

  1. 模擬私有變量膜赃。
    閉包可以用來管理的全局命名空間挺邀,以免在全局范圍內(nèi)共享數(shù)據(jù)。一些庫和模塊還會使用閉包來隱藏整個模塊的私有方法和數(shù)據(jù)跳座。這被稱為模塊模式 端铛,它采用了立即調(diào)用函數(shù)表達(dá)式(IIFE)


    image.png
  2. 異步服務(wù)端調(diào)用 。

  3. 創(chuàng)建人工作作用域變量 疲眷。

5. 函數(shù)鏈

5.1 鏈接方法

方法鏈?zhǔn)且环N能夠在一個語句中調(diào)用多個方法的面向?qū)ο缶幊棠J胶滩稀.?dāng)這些方法屬于同一個對象時,方法鏈又稱為方法級聯(lián), demo如下:

'Functional Programming'.substring(0, 10).toLowerCase() + ' is fun';
// 函數(shù)式風(fēng)格
concat(toLowerCase(substring('Functional Programming', 1, 10))), 'is fun' 

5.2 函數(shù)鏈

函數(shù)式編程則采用了不同的方式狂丝。它不是通過創(chuàng)建一個全新的數(shù)據(jù)結(jié)構(gòu)類型來滿足特定的需求换淆,而是使用如數(shù)組這樣的普通類型哗总,并施加在一套粗粒度的高階操作之上,這些操作是底層數(shù)據(jù)形態(tài)所不可見的倍试。這些操作會作如下設(shè)計讯屈。
? 接收函數(shù)作為參數(shù),以便能夠注入解決特定任務(wù)的特定行為易猫。
? 代替充斥著臨時變量與副作用的傳統(tǒng)循環(huán)結(jié)構(gòu)耻煤,從而減少所要維護(hù)以及可能出錯的代碼。

5.3 lambda 表達(dá)式(箭頭函數(shù))

lambda 表達(dá)式適用于函數(shù)式的函數(shù)定義准颓,因為它總是需要返回一個值哈蝇。對于單行表達(dá)式,其返回值就是函數(shù)體的值攘已。另一個值得注意的是一等函數(shù)與 lambda 表達(dá)式之間的關(guān)系炮赦。函數(shù)名代表的不是一個具體的值,而是一種(惰性計算的)可獲取其值的描述样勃。換句話說吠勘,函數(shù)名指向的是代表著如何計算該數(shù)據(jù)的箭頭函數(shù)。這就是在函數(shù)式編程中可以將函數(shù)作為數(shù)值使用的原因峡眶。

5.4 Lodash惰性計算函數(shù)鏈

image.png

_.chain 函數(shù)可以添加一個輸入對象的狀態(tài)剧防,從而能夠?qū)⑦@些輸入轉(zhuǎn)換為所需輸出的操作鏈接在一起。與簡單地將數(shù)組包裹在(...)對象中不同辫樱,其強(qiáng)大之處在于可以鏈接序列中的任何函數(shù)峭拘。盡管這是一個復(fù)雜的程序,但仍然可以避免創(chuàng)建任何變量 狮暑,并且有效地消除所有循環(huán)鸡挠。
使用 _.chain 的另一個好處是可以創(chuàng)建具有惰性計算能力的復(fù)雜程序,在調(diào)用 value () 前搬男,并不會真正地執(zhí)行任何操作拣展。這可能會對程序產(chǎn)生巨大的影響,因為在不需要其結(jié)果的情況下缔逛,可以跳過運(yùn)行所有函數(shù)

能夠惰性地定義程序的管道不止有可讀性這一個好處备埃。由于以惰性計算方式編寫的程序會在運(yùn)行前定義好,因此可以使用數(shù)據(jù)結(jié)構(gòu)重用或者方法融合等技術(shù)對其進(jìn)行優(yōu)化褐奴。這些優(yōu)化不會減少執(zhí)行函數(shù)本身所需的時間按脚,但有助于消除不必要的調(diào)用

6. 遞歸

遞歸是一種旨在通過將問題分解成較小的自相似問題來解決問題本身的技術(shù),將這些小的自相似問題結(jié)合在一起歉糜,就可以得到最終的解決方案乘寒。遞歸函數(shù)包含以下兩個主要部分望众。
? 基例(也稱為終止條件) :能夠令遞歸函數(shù)計算出具體結(jié)果的一組輸入匪补,而不必再重復(fù)下去
? 遞歸條件:處理函數(shù)調(diào)用自身的一組輸入(必須小于原始值)伞辛。如果輸入不變小,那么遞歸就會無限期地運(yùn)行夯缺,直至程序崩潰蚤氏。隨著函數(shù)的遞歸,輸入會無條件地變小踊兜,最終到達(dá)觸發(fā)基例的條件竿滨,以一個值作為遞歸過程的終止。

事實上捏境,純函數(shù)式語言甚至沒有標(biāo)準(zhǔn)的循環(huán)結(jié)構(gòu)于游,如 do、 for 和 while垫言,因為所有循環(huán)都是遞歸完成的 贰剥。 遞歸使代碼更易理解,因為它是以多次在較小的輸人上重復(fù)相同的操作為基礎(chǔ)的

6.1 尾調(diào)用優(yōu)化

image.png

這個版本的實現(xiàn)將遞歸調(diào)用作為函數(shù)體中最后的步驟筷频,也就是尾部位置蚌成。

6.2 遞歸定義的數(shù)據(jù)結(jié)構(gòu)

遞歸算法執(zhí)行整個樹的先序遍歷,從根開始并且下降到所有子節(jié)點凛捏。由于其自相似性担忧,從根節(jié)點遍歷樹和從任何節(jié)點遍歷子樹是完 全一樣的,這就是遞歸定義坯癣。

樹的先序遍歷按照以下步驟執(zhí)行瓶盛,從根節(jié)點開始。

  1. 顯示根元素的數(shù)據(jù)部分坡锡。
  2. 通過遞歸地調(diào)用先序函數(shù)來遍歷左子樹蓬网。
  3. 以相同的方式遍歷右子樹。


    遞歸的先序遍歷鹉勒,從根節(jié)點開始帆锋,一直向左下降,然后再向右移動

6.3 遞歸的弱點

如果你見過錯誤 Range Error : Maximum Call Stack Exceeded or too much recursion禽额,就會知道遞歸有問題了锯厢。通過下面這個簡單的腳本,可以測試瀏覽器函數(shù)堆棧大概的大小:

function increment (i) { 
  console.log(i); 
  increment(++i);
}
increment(i);

不同的瀏覽器的堆找錯誤會有不同:例如在某臺計算機(jī)上脯倒, Chrome 會在 17500 次遞歸后觸發(fā)異常实辑,而 Firefox 的會遞歸大約 213000 次。不要以這些數(shù)值作為遞歸函數(shù)的上界! 這些數(shù)字只是為了說明遞歸是有限制的藻丢。代碼預(yù)設(shè)應(yīng)該要遠(yuǎn)遠(yuǎn)低于這些閥值剪撬,否則遞歸肯定是有問題的。
遍歷這種異常巨大的數(shù)組的另一種方式就是利用高階函數(shù)悠反,如 map残黑、 filter 以及 reduce馍佑。使用這些函數(shù)不會產(chǎn)生嵌套的函數(shù)調(diào)用,因為堆棧在每次迭代循環(huán)后都能得到回收梨水。
雖然柯里化和遞歸導(dǎo)致更多的內(nèi)存占用拭荤,但是鑒于它們帶來的靈活性和復(fù)用性以及遞歸解決方案固有的正確性 ,又感覺這些額外的內(nèi)存花費(fèi)是值得的疫诽。
另外我們可以通過惰性求值來推遲函數(shù)執(zhí)行

6.4 尾遞歸調(diào)用優(yōu)化

當(dāng)使用尾遞歸時舅世,編譯器有可能幫助你做尾 部調(diào)用優(yōu)化(TCO)


image.png

TCO 也稱為尾部調(diào)用消除 ,是 ES6 添加的編譯器增強(qiáng)功能奇徒。 同時雏亚,在最后的位置調(diào)用別的函數(shù)也可以優(yōu)化(雖然通常是本身),該調(diào)用位置稱為尾部位置 (尾遞歸因此而得名)摩钙。
這為什么算是一種優(yōu)化?函數(shù)的最后一件事情如果是遞歸的函數(shù)調(diào)用评凝,那么運(yùn)行時會認(rèn)為不必要保持當(dāng)前的棧幀,因為所有工作已經(jīng)完成腺律,完全可以拋棄當(dāng)前幀奕短。在大多數(shù)情況下,只有將函數(shù)的上下文狀態(tài)作為參數(shù)傳遞給下一個函數(shù)調(diào)用(正如在遞歸階乘函數(shù)處看到的) 匀钧,才能使遞歸調(diào)用不需要依賴當(dāng)前幀翎碑。通過這種方式,遞歸每次都會創(chuàng)建一個新的幀之斯,回收舊的幀日杈,而不是將新的幀疊在舊的上。因為 factorial 是尾遞歸 的形式佑刷,所以 factorial(4)的調(diào)用會從典型的遞歸金字塔:

factorial(4)
  4 * factorial(4)
    4 * 3 * factorial(2)
      4 * 3 * 2 * factorial(1)
        4 * 3 * 2 * 1 * factorial(0)
          4 * 3 * 2 * 1 * 1
        4 * 3 * 2 * 1
       4 * 3 * 2
    4 * 6
return 24

相對于如下上下文堆棧

factorial(4)
  factorial(3, 4)
  factorial(2, 12)
  factorial(1, 24)
  factorial(0, 24)
  return 24
return 24
尾遞歸 factorial(4)求值的詳細(xì)視圖莉擒。函數(shù)只使用了一幀。TCO 負(fù)責(zé)拋棄當(dāng)前幀瘫絮,為新的幀讓路涨冀,就像 factorial 在循環(huán)中求值一樣

6.5 將非尾遞歸轉(zhuǎn)為尾遞歸

const factorial = (n) => (n===1)? 1 : (n * factorial(n - 1));

遞歸調(diào)用并沒有發(fā)生在尾部,因為最后返回的表達(dá)式是 n * factorial (n - 1)麦萤。 切記鹿鳖,最后一個步驟一定要是遞歸,這樣才會在運(yùn)行時 TCO 將 factorial 轉(zhuǎn)換成一 個循環(huán)壮莹。改成尾遞歸只需要兩步翅帜。

  1. 將當(dāng)前乘法結(jié)果當(dāng)作參數(shù)傳人遞歸函數(shù)。
  2. 使用 ES6 的默認(rèn)參數(shù)給定一個默認(rèn)值(也可以部分地應(yīng)用它們命满,但默認(rèn)參數(shù)會讓代碼更整潔 )涝滴。
const factorial = (n, current = 1) => (n === 1) ? current : factorial(n - 1, n * current) ,

這個階乘函數(shù)運(yùn)行起來眼標(biāo)準(zhǔn)循環(huán)沒什么區(qū)別,沒有額外創(chuàng)建堆棧幀,同時仍保留了它原本的數(shù)學(xué)聲明的感覺歼疮。之所以能夠做這種轉(zhuǎn)換僵娃,是因為尾遞歸函數(shù)跟循環(huán)有著共同的特點


標(biāo)準(zhǔn)循環(huán)(左)及其等效的尾遞歸函數(shù)之間的相似之處。在這兩個代碼示例中腋妙,讀者可以很容易地找到基例、事后操作讯榕、累計參數(shù)和結(jié)果
function sum (arr) {
  if (_.isEmpty(arr)) {
    return 0;
  }
  return _.first(arr) + sum(_.rest(arr));
}
// 轉(zhuǎn)為尾調(diào)用
function sum(arr, ace = 0) { 
  if(_.isEmpty(arr)) {
    return 0;
  }
  return sum(rest(arr), ace+_.first(arr));
}

尾遞歸帶來遞歸循環(huán)的性能接近于 for 循環(huán)骤素。所以對于有尾遞歸優(yōu)化的語言,比如 ES6愚屁,就可以在保持算法的正確性和 mutation 的控制济竹,同時還能保持不會拖累性能。不過尾調(diào)用也不僅限于尾遞歸 霎槐。 也可以是調(diào)用另一個函數(shù)送浊,這種情況在 JavaScript 代碼中也很常見。不過要注意的是丘跌,這是個新的 JavaScript標(biāo)準(zhǔn)袭景,即便 ES4就開始起草,很多瀏覽器也沒有廣泛實現(xiàn)闭树。

還有一種解決方式是使用 trampolining耸棒。 trampolining 可以用迭代的方式模擬尾遞歸,所以可以非常理想报辱、容易地控制 JavaScript 的堆棧与殃。
trampolining是一個接受函數(shù)的函數(shù),它會多次調(diào)用函數(shù)碍现,直到滿足一定的條件幅疼。一個可反彈或者重復(fù)的函數(shù)被封裝在 thunk 結(jié)構(gòu)中。 thunk 只不過是多了 一層函數(shù)包裹昼接。在函數(shù)式 JavaScript背景下爽篷,可以用 thunk及簡單的匿名函數(shù)包裹期望惰性求值的值。
要檢查 TCO 和其他 ES6 特性的兼容性慢睡,可以登錄: https://kangax.github.io/compat table/es6/狼忱。

7. 函數(shù)的管道化

? 方法鏈接(緊耦合,有限的表現(xiàn)力) 一睁。
? 函數(shù)的管道化(松耦合钻弄,靈活)。


image.png

比較命令式代碼者吁,這的確是一個能夠極大提高代碼可讀性的語法改進(jìn)窘俺。然而,它與方法所屬的對象緊緊地耦合在一起 , 限制鏈中可以使用的方法數(shù)量 瘤泪,也就限制了代碼的表現(xiàn)力灶泵。這樣就只能夠使用由 Lodash 提供的操作,而無法輕松地將不同函數(shù)庫的(或自定義的)函數(shù)連接在一起对途。

7.1 兼容條件

由于 JavaScirpt 是弱類型語言赦邻,因此從類型角度來看,無須像使用一些靜態(tài)類型語言一樣太過關(guān)注類型实檀。因此惶洲,如果一個對象在應(yīng)用中表現(xiàn)得像某個特定類型, 那么它就是該類型膳犹。這也被稱為鴨子類型 : “如果走起來像鴨子恬吕,并且像鴨子一樣叫,那這就是一只鴨子须床☆砹希”
正式地講,僅當(dāng) f 的輸出類型等同于函數(shù) g 的輸入時豺旬,兩個函數(shù) f 和 g 是類型兼容的钠惩。舉例來說, 一個處理學(xué)生社會安全號碼的簡單程序 :


image.png

使用 trim和normalize手動構(gòu)建函數(shù)管道


手動構(gòu)建函數(shù)管道

7.2 元數(shù)

元數(shù)定義為函數(shù)所接收的參數(shù)數(shù)量 族阅,也被稱為函數(shù)的長度
如何返回兩個不同的值呢?函數(shù)式語言通過一個稱為元組的結(jié)構(gòu)來做到這一點妻柒。元組是有限的、有序的元素列表耘分。
來看一個代碼示例:

return {
  status: false, 
  message: "Input is a too long"
}
// or
 return [false, "Input is too long"]; 

8. 函數(shù)執(zhí)行機(jī)制

全局上下文幀永遠(yuǎn)駐留在堆棧的底部举塔。每個函數(shù)的上下文幀都占用一定量的內(nèi)存,實際取決于其中的局部變量的個數(shù)求泰。如果沒有任何局部變量央渣,一個空幀大約 48 個字節(jié)。每個數(shù)字或布爾類型的局部變量和參數(shù)會占用 8字節(jié)渴频。所以芽丹,函數(shù)體聲明越多的變量,就需要越大的堆棧幀卜朗。 每一幀大致包含以下信息:


函數(shù)執(zhí)行機(jī)制

注意 :函數(shù)的作用域鏈與 JavaScript 對象的原型鏈不是一回事拔第。 雖然兩者表現(xiàn)得很類似,但是原型鏈通過 prototype屬性建立對象繼承的鏈接场钉,而作用域鏈?zhǔn)侵竷?nèi)部函數(shù)能訪問到外部函數(shù)的閉包蚊俺。
堆棧的行為由下列規(guī)則確定。
? JavaScript 是單線程的逛万,這意味著執(zhí)行的同步性 迟蜜。
? 有且只有一個全局上下文(與所有函數(shù)的上下文共享) 求类。
? 函數(shù)上下文的數(shù)量是有限制的(對客戶端代碼码党,不同的瀏覽器可以有不同的限制)。
? 每個函數(shù)調(diào)用會創(chuàng)建一個新的執(zhí)行上下文纵潦,遞歸調(diào)用也是如此。

9. 柯里化與函數(shù)上下文堆棧

柯里化函數(shù)時,把一次函數(shù)執(zhí)行變成了多次執(zhí)行的函數(shù)(每次消費(fèi)一個參數(shù)),來看一個logger函數(shù)

const logger = function (appender, layout, name, level, message)

柯里化后會變成如下嵌套結(jié)構(gòu)

const l ogger =
function (appender) {
  return function (layout) ( 
    return function (name) {
      return function (level) { 
        return function (message) {

嵌套結(jié)構(gòu)的函數(shù)會使用更多的堆棧拷呆。 先來解釋 logger 函數(shù)的非柯里化的執(zhí)行。 由于 JavaScript 的同步執(zhí)行機(jī)制疫粥,調(diào)用 logger 會暫停全局上下文的執(zhí)行茬斧, 好讓 logger 運(yùn)行, 創(chuàng)建新的活躍上下文手形,并引用全局上下文中的所有變量。


image.png

調(diào)用任何函數(shù)時悯恍,如 logger库糠,單線程 JavaScript 運(yùn)行時會暫停當(dāng)前全局上下文并激活新函數(shù)創(chuàng)建的上下文 。 此時涮毫,還會通過 scopeChain 創(chuàng)建到全局上下文的鏈接瞬欧。
一旦 logger 返回,它的執(zhí)行上下文也會被彈出堆棧罢防,全局上下文將恢復(fù)

當(dāng) logger 函數(shù)調(diào)用其他函數(shù) (如 Log4js)時艘虎, 會在堆棧上產(chǎn)生新函數(shù)的上下文。 由于 JavaScript的閉包咒吐,內(nèi)部函數(shù)調(diào)用的上下文會在外部函數(shù)上下文堆棧的上面占用分配給它的存儲器野建, 并經(jīng)由 scopeChain 鏈接起來


運(yùn)行嵌套函數(shù)時函數(shù)上下文的變化 。 因為每個函數(shù)會產(chǎn)生新的堆棧幀恬叹, 所以堆棧增長跟函數(shù)嵌套的層級成正比候生。 柯里化與遞歸都依賴于嵌套的函數(shù)調(diào)用

一旦 Log4js 代碼運(yùn)行完,它就會被彈出堆棧 ; logger 函數(shù)也會在之后被彈出绽昼, 運(yùn)行時環(huán)境恢復(fù)到只有全局上下文的狀態(tài)唯鸭。這就是 JavaScript 的閉包背后的魔法。
雖然這種方法強(qiáng)大硅确,但是嵌套深的函數(shù)會消耗大量的內(nèi)存目溉。

柯里化版本:


柯里化將每一個參數(shù)都轉(zhuǎn)換成內(nèi)部嵌套調(diào)用×馀可以連續(xù)提供參數(shù)帶來了靈活性缭付,卻額外占用了堆棧空間

柯里化所有函數(shù)看起來是不錯的主意循未,但是過度使用會導(dǎo)致其占用較大的堆楎入纾空間,進(jìn)而導(dǎo)致程序運(yùn)行速度顯著降低。

10. 惰性求值

當(dāng)輸人很大但只有一個小的子集有效時烙丛, 避免不必要的函數(shù)調(diào)用可以體現(xiàn)出許多性能優(yōu)勢舅巷,例如函數(shù)式語言 Haskell 就內(nèi)置了惰性函數(shù)求值
JavaScript 使用的是更主流的函數(shù)求值策略一一及早求值。及早求值會在表達(dá)式綁定到變量時求值河咽,不管結(jié)果是否會被用到钠右,所以也稱為貪婪求值


image.png

10.1 緩存(記憶化)

image.png

10.2 給函數(shù)添加記憶化

image.png

10.3 記憶化遞歸調(diào)用

image.png

由于記憶化了階乘函數(shù),在第二次迭代時吞吐量有顯著的提升忘蟹。在第二次運(yùn)行時飒房,函 數(shù)“記住”了使用公式“101!=101×100!”,并且可以重復(fù)使用 factorial(lOO)的值媚值, 使得整個算法立即返回狠毯,并對戰(zhàn)幀的管理以及污染堆棧方面都有好處


運(yùn)行記憶化的 factorial(lOO)在第一次會創(chuàng)建 100 的堆棧幀 , 因為它需要計算 100!在第二次 調(diào)用 101 的階乘時通過記憶化能夠重復(fù)使用 factorial (100)的結(jié)果褥芒, 所以只會創(chuàng)建 2個核幀

10.4 生成惰性數(shù)據(jù)

ES6 最強(qiáng)大的功能之一是可以通過暫停函數(shù)執(zhí)行而不用一次運(yùn)行完嚼松。這帶來了許多(可能無限的)機(jī)會,比如函數(shù)可以生成惰性數(shù)據(jù)锰扶,而不必一次處理大量的數(shù)據(jù)献酗。這稱為生成器(generator)。
下面來看一個簡單的例子坷牛,該例只取前 3 個元素罕偎,而不會產(chǎn)生無數(shù)的列表 :


image.png

使用生成器,可以惰性地從無限集中取一定數(shù)量的元素:

function take (amount , generator) { 
  let result= [];
  for (let n of generator) {
    result.push(n);
    if(n ===amount) { break };
  }
  return result
}
take(3, range(1, Infinity)); 11-> [1, 2, 3]

除了一些限制京闰,生成器的行為與任何標(biāo)準(zhǔn)函數(shù)調(diào)用非常相似颜及。可以通過給它傳遞參數(shù)蹂楣,(也許是一個函數(shù))來操作生成的值:


image.png

11. 生成器

11.1 生成器與遞歸

就像任何函數(shù)調(diào)用 一樣器予,也可以在生成器中調(diào)用其他生成器。這對于將嵌套對象集合扁平化非常有用捐迫,比如樹的遍歷乾翔。因為可以用 for...of 遍歷生成器,調(diào)用另一個生成器就類似于合并兩個集合并遍歷施戴。


每個節(jié)點代表一個 student 對象反浓,每個箭頭代表“學(xué)生關(guān)系”

可以使用生成器輕松地對這棵樹進(jìn)行建模


image.png

下面的代碼使用遞歸遍歷這棵樹(每個節(jié)點包含一個 Person 對象):
image.png

11.2 迭代器協(xié)議

生成函數(shù)返回符合迭代器協(xié)議的 Generator 對象。這意味著它實現(xiàn)一個名為 next ()的方法赞哗,該方法返回使用 yield 關(guān)鍵字 return 的值雷则。此對象具有以下屬性。
? done一一如果迭代器到達(dá)序列結(jié)尾肪笋,則值為 true;否則月劈,值為 false度迂,表示迭代器還可以生成下一個值 。
? value一一迭代器返回的值猜揪。


image.png

可以以這種形式創(chuàng)建符合某種規(guī)范的數(shù)據(jù)惭墓。例如平方生成器:


image.png

JavaScript 中有許多內(nèi)含@@iterator屬性的可迭代對象。數(shù)組是可以這樣使用的 :

var iter = ['s', 't', 'r', 'e', 'a'][Symbol.iterator]()
iter.next().value // s
iter.next().value // t

字符串也可以迭代:

var iter= ’Stream’[Symbol.iterator](); 
iter.next() .value // -> S
 iter.next().value // -> t

12. Observable

Rx.Observable 對象將函數(shù)式編程與響應(yīng)式編程結(jié)合在一起


image.png
從 Observable應(yīng)用函數(shù) filter 和 map 的過程

響應(yīng)式編程傾向于和函數(shù)式編程一起使用而姐,從而產(chǎn)生函數(shù)式晌應(yīng)式編程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腊凶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拴念,更是在濱河造成了極大的恐慌钧萍,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件政鼠,死亡現(xiàn)場離奇詭異风瘦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)公般,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門万搔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人俐载,你說我怎么就攤上這事蟹略〉鞘В” “怎么了遏佣?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長揽浙。 經(jīng)常有香客問我状婶,道長,這世上最難降的妖魔是什么馅巷? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任膛虫,我火速辦了婚禮,結(jié)果婚禮上钓猬,老公的妹妹穿的比我還像新娘稍刀。我一直安慰自己,他們只是感情好敞曹,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布账月。 她就那樣靜靜地躺著,像睡著了一般澳迫。 火紅的嫁衣襯著肌膚如雪局齿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天橄登,我揣著相機(jī)與錄音抓歼,去河邊找鬼讥此。 笑死,一個胖子當(dāng)著我的面吹牛谣妻,可吹牛的內(nèi)容都是我干的萄喳。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼拌禾,長吁一口氣:“原來是場噩夢啊……” “哼取胎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起湃窍,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤闻蛀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后您市,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體觉痛,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年茵休,在試婚紗的時候發(fā)現(xiàn)自己被綠了薪棒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡榕莺,死狀恐怖俐芯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钉鸯,我是刑警寧澤吧史,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站唠雕,受9級特大地震影響贸营,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜岩睁,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一钞脂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捕儒,春花似錦冰啃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至栋猖,卻和暖如春净薛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蒲拉。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工肃拜, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留痴腌,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓燃领,卻偏偏與公主長得像士聪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子猛蔽,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345