函數(shù)式編程無疑是目前的熱門技術(shù)話題之一意鲸,值得每個開發(fā)者認(rèn)真研究。iOS觉既、Android這幾年不約而同的都更換了編程語言昂羡,從Objective-C到Swift,從Java到Kotlin鸣皂,一個明顯的改變就是抓谴,語言不再是純粹面向?qū)ο蟮哪乎澹技尤雽瘮?shù)式編程的支持。
讀過《代碼大全》的朋友應(yīng)該對“軟件復(fù)雜度”一詞印象深刻癌压,“軟件復(fù)雜度”估計是全書出現(xiàn)頻率最高的詞了椎侠,也是技術(shù)發(fā)展的核心因變量。很多編程技巧措拇、思想都是為了控制和降低軟件復(fù)雜度我纪。
復(fù)雜度就是任何使軟件難以理解或修改的東西∝は牛— John Outerhout
回想一下被自己或同事的一大坨代碼折磨的可怕情景浅悉,馬上要發(fā)版上線了,突然發(fā)現(xiàn)有一個bug需要修改券犁,令人沮喪的是术健,代碼千纏百結(jié),動一發(fā)而牽全身粘衬,只能硬著頭皮碰運氣荞估。有什么解決辦法嗎?函數(shù)式編程或許有所幫助稚新。
函數(shù)式編程中的不可變性勘伺、純函數(shù)等概念幫助我們消除了函數(shù)的副作用,有效降低了代碼的復(fù)雜度褂删。
什么是函數(shù)式編程飞醉?
函數(shù)式編程是一種編程范式(構(gòu)建計算機(jī)程序結(jié)構(gòu)和元素的風(fēng)格),將計算視為函數(shù)的求值屯阀,避免狀態(tài)變化和可變數(shù)據(jù)缅帘。— 維基百科
一種聲明式編程范式难衰,因為編程是用表達(dá)式或聲明而不是語句來完成的钦无。
函數(shù)式代碼是冪等的:函數(shù)的返回值只依賴于其參數(shù),相同的參數(shù)永遠(yuǎn)產(chǎn)生相同的返回值盖袭。相反失暂,在命令式編程中,除了函數(shù)的參數(shù)苍凛,全局狀態(tài)也會影響函數(shù)的返回值趣席。消除副作用,即不依賴于函數(shù)輸入的狀態(tài)變化醇蝴,可以使代碼更容易理解宣肚,這是函數(shù)式編程的核心動機(jī)。
前面的話翻譯自維基百科悠栓,有些過于書面化霉涨。下里巴人的話是:
函數(shù)式編程是通過純函數(shù)的組合來構(gòu)建軟件的一種編程方式按价,強(qiáng)調(diào)避免共享狀態(tài)、可變數(shù)據(jù)和副作用笙瑟。函數(shù)式編程是聲明式的楼镐,而不是命令式的,應(yīng)用狀態(tài)通過純函數(shù)流動往枷。對比面向?qū)ο缶幊炭虿鋺?yīng)用狀態(tài)通常是共享的,并且與對象中的方法關(guān)聯(lián)错洁。
函數(shù)式編程是一種編程范式秉宿,也就是說,它是基于一些基本的屯碴、決定性的原則來構(gòu)建軟件的一種方法描睦。其它的常見編程范式包括面向?qū)ο蟆⒚嫦蜻^程导而、面向協(xié)議等忱叭。注意函數(shù)式編程與面向過程編程的區(qū)別,很多人錯誤地認(rèn)為傳統(tǒng)的C語言編程就是函數(shù)式編程今艺,真是讓人啼笑皆非韵丑。
函數(shù)式代碼通常比命令式或面向?qū)ο蟮拇a更簡潔、可預(yù)測性更強(qiáng)洼滚、更易于測試 -- 不過埂息,如果你對函數(shù)式編程和其常見模式不了解,函數(shù)式代碼也可能看起來頭疼遥巴,因為其信息密度更高。
所有的聲明式編程享幽,概念都相對比較多铲掐,用過Rx的應(yīng)該深有體會。函數(shù)式編程也不例外值桩,涉及到的概念比較多摆霉,學(xué)習(xí)曲線比較陡,隨便來幾個概念感受一下:Monad奔坟、Applicative携栋、Functor。生僻的單詞有沒有激起你想逃跑的本能咳秉?
不要被生僻詞嚇跑婉支,它并沒有看起來那么恐怖。
暫且不用管Monad
這種看起來似乎很深奧的家伙澜建,作為初學(xué)者向挖,只要先掌握以下概念即可:
- 純函數(shù)
- 函數(shù)組合
- 避免共享狀態(tài)
- 避免可變狀態(tài)
- 避免副作用
請牢記這幾個概念蝌以,這是函數(shù)式編程的核心。
純函數(shù)
函數(shù)式編程的第一個基礎(chǔ)概念就是純函數(shù)何之。純函數(shù)是什么跟畅?如何使函數(shù)變“純”?
如何判斷一個函數(shù)是否為純函數(shù)溶推?對“純”的定義如下:
- 參數(shù)相同徊件,則返回值相同(也稱為
確定性
) - 不會引起任何可觀察的副作用
參數(shù)相同,則返回值相同
讓我們來實現(xiàn)一個計算圓形面積的函數(shù)蒜危。一種非純函數(shù)的實現(xiàn)方式是庇忌,接受一個radius
參數(shù),然后返回radius * radius * PI
:
let PI = 3.14;
const calculateArea = (radius) => radius * radius * PI;
calculateArea(10); // returns 314.0
為什么這不是純函數(shù)舰褪?因為它使用了一個全局變量皆疹,而不是將其作為參數(shù)傳入。
假設(shè)我們需要提高PI
的精度占拍,將其改為3.1415926
略就,對于同樣的參數(shù)10
,上面函數(shù)的返回值將改變晃酒。
重寫一下:
let PI = 3.14;
const calculateArea = (radius, pi) => radius * radius * pi;
calculateArea(10, PI); // returns 314.0
現(xiàn)在我們將PI
作為參數(shù)傳入表牢,除了輸入?yún)?shù),函數(shù)不訪問任何外部對象贝次。毫無疑問崔兴,對于相同的參數(shù),函數(shù)必然返回相同的結(jié)果蛔翅。
讀取文件
如果函數(shù)讀取外部文件敲茄,則函數(shù)不是純函數(shù) — 文件的內(nèi)容可能會改變。
const charactersCounter = (text) => `Character count: ${text.length}`;
function analyzeFile(filename) {
let fileContent = open(filename);
return charactersCounter(fileContent);
}
隨機(jī)數(shù)
任何依賴隨機(jī)數(shù)生成的函數(shù)都不是純函數(shù)山析。
function yearEndEvaluation() {
if (Math.random() > 0.5) {
return "You get a raise!";
} else {
return "Better luck next year!";
}
}
不產(chǎn)生可觀察的副作用
副作用是除了返回值以外堰燎,在被調(diào)用函數(shù)之外可觀察到的任何狀態(tài)變化。包括:
- 修改外部變量或?qū)ο蟮膶傩裕ɡ缢窆欤肿兞扛鸭簦负瘮?shù)范圍內(nèi)的變量)
- 控制臺日志輸出
- 屏幕輸出
- 寫文件
- 輸出到網(wǎng)絡(luò)
- 調(diào)用外部進(jìn)程
- 調(diào)用其它含有副作用的函數(shù)
Haskell和其它函數(shù)式編程語言通常使用monads從純函數(shù)中隔離和封裝副作用。monads
的概念比較深爵政,足夠?qū)懸槐緯私龇恚信d趣的可以自己查資料了解。
現(xiàn)在我們需要知道的是钾挟,副作用需要與軟件的其它部分隔離洁灵。如果將副作用與其它代碼邏輯分開,軟件將更易于擴(kuò)展等龙、重構(gòu)处渣、調(diào)試伶贰、測試和維護(hù)。這也是大多數(shù)前端框架鼓勵將狀態(tài)管理和組件繪制獨立罐栈、松散耦合地維護(hù)的原因黍衙。
假設(shè)我們要實現(xiàn)一個對整數(shù)加1的函數(shù)。
let counter = 1;
function increaseCounter(value) {
counter = value + 1;
}
increaseCounter(counter);
console.log(counter); // 2
純函數(shù)實現(xiàn):
let counter = 1;
const increaseCounter = (value) => value + 1;
increaseCounter(counter); // 2
console.log(counter); // 1
函數(shù)式編程不鼓勵可變性荠诬。
如果我們遵循這兩個簡單的原則琅翻,我們的代碼會變得更容易理解。每個函數(shù)都是隔離的柑贞,不會影響系統(tǒng)的其它部分方椎。
純函數(shù)是穩(wěn)定的、一致的和可預(yù)測的钧嘶。給定相同的參數(shù)棠众,純函數(shù)始終返回相同的結(jié)果。我們不需要考慮相同參數(shù)而結(jié)果不同的情況— 因為它永遠(yuǎn)不會發(fā)生有决。
純函數(shù)的好處
代碼明顯更易于測試闸拿。不需要模擬任何東西,所以我們可以用不同的上下文對純函數(shù)進(jìn)行單元測試:
- 給定參數(shù)
A
书幕,期望返回值B
- 給定參數(shù)
C
新荤,期望返回值D
一個簡單的例子是,函數(shù)接受一個數(shù)字的列表台汇,對列表中的每個數(shù)字加1:
let list = [1, 2, 3, 4, 5];
const incrementNumbers = (list) => list.map(number => number + 1);
接受numbers
數(shù)組苛骨,使用map
對每個數(shù)字加1,返回新的數(shù)字列表苟呐。
incrementNumbers(list); // [2, 3, 4, 5, 6]
對于輸入[1, 2, 3, 4, 5]
痒芝,期望的輸出是[2, 3, 4, 5, 6]
。
不可變性
不隨時間而改變掠抬,或不可改變吼野。
當(dāng)數(shù)據(jù)不可變時,其狀態(tài)在創(chuàng)建之后不可更改两波。如果需要改變一個不可變對象,那就重新創(chuàng)建一個闷哆。
我們經(jīng)常使用for
循環(huán)腰奋,for
循環(huán)中有幾個變量:
var values = [1, 2, 3, 4, 5];
var sumOfValues = 0;
for (var i = 0; i < values.length; i++) {
sumOfValues += values[i];
}
sumOfValues // 15
每次迭代,改變i
和sumOfValue
的狀態(tài)抱怔。
如果要求不改變外部狀態(tài)劣坊,應(yīng)該如何做呢?用遞歸屈留。
let list = [1, 2, 3, 4, 5];
let accumulator = 0;
function sum(list, accumulator) {
if (list.length == 0) {
return accumulator;
}
return sum(list.slice(1), accumulator + list[0]);
}
sum(list, accumulator); // 15
list; // [1, 2, 3, 4, 5]
accumulator; // 0
初看可能不太容易理解局冰,sub
函數(shù)接受一個數(shù)字列表以及當(dāng)前累加值测蘑,每次迭代將第一個元素加到累加值上,然后用其它元素和新的累加值進(jìn)行下一輪迭代康二,直到列表為空碳胳。
通過使用迭代,我們保持了狀態(tài)的不變性沫勿。list
和accmulator
在運算過程中沒有發(fā)生變化挨约。
可以使用reduct
實現(xiàn)此函數(shù),見后面高階函數(shù)部分产雹。
注意:此處的代碼其實變得更難以理解了诫惭,這也是純函數(shù)式編程難以流行的原因。真正的高手蔓挖,打的都是組合拳夕土,適可而止很重要,否則過猶不及瘟判。
對各種對象做轉(zhuǎn)換是很常見的任務(wù)怨绣。比如,有一個字符串荒适,我們需要將其轉(zhuǎn)換為url slug
(看一下瀏覽器的地址欄梨熙,用-
分割的名稱就是url slug
)。
下面是Ruby的OOP實現(xiàn)刀诬,我們創(chuàng)建一個類UrlSlugify
咽扇,此類有一個slugify
方法用來實現(xiàn)此轉(zhuǎn)換。
class UrlSlugify
attr_reader :text
def initialize(text)
@text = text
end
def slugify!
text.downcase!
text.strip!
text.gsub!(' ', '-')
end
end
UrlSlugify.new(' I will be a url slug ').slugify! # "i-will-be-a-url-slug"
此處是命令式編程陕壹,明確說明了slugify
的步驟 — 首先轉(zhuǎn)小寫质欲,然后移除無用的空格,最后用連字符替換剩余的空格糠馆。
在處理過程中嘶伟,我們改變了輸入狀態(tài)。
不改變輸入狀態(tài)的方式應(yīng)該如何實現(xiàn)又碌?可以用函數(shù)組合或函數(shù)鏈九昧。函數(shù)的結(jié)果作為下一個函數(shù)的輸入,而不改變原始輸入毕匀。
const string = " I will be a url slug ";
const slugify = string =>
string
.toLowerCase()
.trim()
.split(" ")
.join("-");
slugify(string); // i-will-be-a-url-slug
此處:
-
toLowerCase
:將字符串轉(zhuǎn)化為小寫 -
trim
:移除字符串兩端的空白字符 -
split
和join
:替換匹配字符
通過以上四個函數(shù)的組合铸鹰,我們實現(xiàn)了slugify
。
引用透明性
我們來實現(xiàn)一個平方函數(shù):
const square = (n) => n * n;
給定相同的輸入皂岔,此純函數(shù)總是返回相同的結(jié)果蹋笼。
square(2); // 4
square(2); // 4
square(2); // 4
// ...
給定參數(shù)2
,square
函數(shù)總是返回4。所以我們可以用4替換square(2)
剖毯,這就叫引用透明性
圾笨。
基本上,如果一個函數(shù)對于相同的輸入逊谋,總是產(chǎn)生相同的結(jié)果擂达,那么就可以說它是引用透明的。
純函數(shù) + 不可變數(shù)據(jù) = 引用透明性
有了這個概念涣狗,我們就可以將函數(shù)“記憶化”(一種程序優(yōu)化技術(shù)谍婉,通過緩存復(fù)雜函數(shù)的結(jié)果,當(dāng)輸入相同時直接返回緩存結(jié)果镀钓,以此來提升程序的運行速度)穗熬。
假設(shè)有以下函數(shù):
const sum = (a, b) => a + b;
有如下調(diào)用:
sum(3, sum(5, 8));
sum(5, 8)
等于13
,此函數(shù)的結(jié)果總是13
丁溅。因此上面的表達(dá)式等于:
sum(3, 13);
此表達(dá)式的結(jié)果總是16
唤蔗。我們可以將整個表達(dá)式用一個數(shù)字常量來替換,將其記憶化窟赏。
函數(shù)是第一類對象
函數(shù)是第一類對象妓柜,是說函數(shù)被作為值對待,可作為普通數(shù)據(jù)使用涯穷。
函數(shù)作為第一類對象使得可以:
- 使用常量或變量引用它
- 將其作為參數(shù)傳入其它函數(shù)
- 將其作為函數(shù)返回值
此思想將函數(shù)視為值棍掐,并作為數(shù)據(jù)傳遞。通過這種方式拷况,我們可以組合不同的函數(shù)來創(chuàng)建具有新行為的新函數(shù)作煌。
假設(shè)有一個函數(shù),對兩個輸入值求和并加倍:
const doubleSum = (a, b) => (a + b) * 2;
另一個函數(shù)赚瘦,對兩個輸入值求差值并加倍:
const doubleSubtraction = (a, b) => (a - b) * 2;
兩個函數(shù)邏輯類似粟誓,不同之處在于運算符函數(shù)。如果我們可以將函數(shù)作為值起意,并作為參數(shù)傳遞鹰服,我們可以構(gòu)建一個函數(shù),運算符函數(shù)作為其參數(shù)揽咕。
const sum = (a, b) => a + b;
const subtraction = (a, b) => a - b;
const doubleOperator = (f, a, b) => f(a, b) * 2;
doubleOperator(sum, 3, 1); // 8
doubleOperator(subtraction, 3, 1); // 4
此處有運算符函數(shù)參數(shù)f
悲酷,用于處理參數(shù)a
和b
。通過sum
和subtraction
函數(shù)與doubleOperator
函數(shù)的組合創(chuàng)造了新的行為亲善。
高階函數(shù)
當(dāng)我們討論高階函數(shù)時舔涎,我們指的是這樣一個函數(shù):
- 將一個或多個函數(shù)作為參數(shù),或
- 返回一個函數(shù)作為返回值
上面的doubleOperator
函數(shù)是一個高階函數(shù)逗爹,因為其將運算符函數(shù)作為參數(shù)。
你可能已經(jīng)聽說過filter
、map
和reduce
掘而,最常用的三個操作容器(數(shù)組挟冠、字典等)的高階函數(shù),我們再一起看一下袍睡。
Filter
給定一個集合知染,我們希望根據(jù)某個屬性做篩選。filter
函數(shù)期望一個布爾值斑胜,用于判定元素是否應(yīng)該包含在結(jié)果集合中控淡。如果回調(diào)函數(shù)返回true
,那么當(dāng)前元素將會包含在結(jié)果集合中止潘,否則不會掺炭。
一個簡單的例子是,我們希望從一個整數(shù)列表中篩選出所有偶數(shù)凭戴。
命令式
命令式的實現(xiàn)方式:
- 創(chuàng)建一個空的
evenNumbers
數(shù)組 - 遍歷
numbers
數(shù)組 - 將偶數(shù)放入
evenNumbers
數(shù)組中
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var evenNumbers = [];
for (var i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 == 0) {
evenNumbers.push(numbers[i]);
}
}
console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]
我們也可以使用filter
高階函數(shù)涧狮,向其傳遞一個even
函數(shù):
const even = n => n % 2 == 0;
const listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10]
在Hacker Rank FP(類似于LeetCode、琶捶颍客網(wǎng))上有一個Filter數(shù)組問題者冤,過濾一個整數(shù)數(shù)組,輸出小于x
的所有數(shù)字档痪。
一種命令式解法可以是:
var filterArray = function(x, coll) {
var resultArray = [];
for (var i = 0; i < coll.length; i++) {
if (coll[i] < x) {
resultArray.push(coll[i]);
}
}
return resultArray;
}
console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0]
精確地說明函數(shù)需要做什么 - 遍歷集合涉枫,將當(dāng)前數(shù)字與x
比較,如果滿足條件則放入結(jié)果數(shù)組resultArray
腐螟。
聲明式
如果我們想要使用filter
高階函數(shù)的更加聲明化的解法呢愿汰?
一種聲明式的解法可以是:
function smaller(number) {
return number < this;
}
function filterArray(x, listOfNumbers) {
return listOfNumbers.filter(smaller, x);
}
let numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0];
filterArray(3, numbers); // [2, 1, 0]
在smaller
函數(shù)中使用this
初看起來可能有點兒怪,不過它本身很好理解 (JS的騷語法遭垛,其它常見編程語言未必有類似用法)尼桶。filter
函數(shù)的第二個參數(shù)會成為this
,在上例中锯仪,this
表示3(x
)泵督。
也可以這樣操作maps。假設(shè)有一個包含name
和age
的map:
let people = [
{ name: "TK", age: 26 },
{ name: "Kaio", age: 10 },
{ name: "Kazumi", age: 30 }
];
我們想要篩選出所有21歲以上的人:
const olderThan21 = person => person.age > 21;
const overAge = people => people.filter(olderThan21);
overAge(people); // [{ name: 'TK', age: 26 }, { name: 'Kazumi', age: 30 }]
Map
map
用來對集合做轉(zhuǎn)換庶喜。
map
將一個函數(shù)應(yīng)用于集合的所有元素小腊,根據(jù)函數(shù)的結(jié)果構(gòu)建一個新的集合,以實現(xiàn)對集合的轉(zhuǎn)換久窟。
還是上面的people
數(shù)組秩冈,不過此次不是根據(jù)年齡做篩選,而是想得到每個人的描述斥扛,類似TK is 26 years old
入问。也就是說,對于每個人,我們希望得到一個字符串:name is :age years old
芬失,其中:name
和:age
是people
數(shù)組中每個元素的屬性楣黍。
一個命令式的實現(xiàn)方式:
var people = [
{ name: "TK", age: 26 },
{ name: "Kaio", age: 10 },
{ name: "Kazumi", age: 30 }
];
var peopleSentences = [];
for (var i = 0; i < people.length; i++) {
var sentence = people[i].name + " is " + people[i].age + " years old";
peopleSentences.push(sentence);
}
console.log(peopleSentences); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']
而聲明式的實現(xiàn)方式可能是:
const makeSentence = (person) => `${person.name} is ${person.age} years old`;
const peopleSentences = (people) => people.map(makeSentence);
peopleSentences(people);
// ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']
Hacker Rank上有一個修改列表問題,將數(shù)組中的每個數(shù)字修改為其絕對值棱烂。
例如租漂,對于數(shù)組[1, 2, 3, -4, 5]
,結(jié)果應(yīng)該是[1, 2, 3, 4, 5]
颊糜。
一種簡單的實現(xiàn)方案是對集合中的每個數(shù)進(jìn)行就地更新:
var values = [1, 2, 3, -4, 5];
for (var i = 0; i < values.length; i++) {
values[i] = Math.abs(values[i]);
}
console.log(values); // [1, 2, 3, 4, 5]
我們使用Math.abs
函數(shù)將每個數(shù)轉(zhuǎn)換為其絕對值哩治,然后就地更新。
這不是函數(shù)式的實現(xiàn)方式衬鱼。
我們前面介紹了不可變性业筏,強(qiáng)調(diào)了不可變性對函數(shù)一致性、可預(yù)測性的重要作用馁启。既然想要構(gòu)建一個絕對值組成的新集合驾孔,為什么不用map
呢?
首先看一下Math.abs
函數(shù)如何操作一個數(shù)字:
Math.abs(-1); // 1
Math.abs(1); // 1
Math.abs(-2); // 2
Math.abs(2); // 2
Math.abs
將數(shù)字轉(zhuǎn)換為其絕對值惯疙。
我們已經(jīng)知道如何獲取一個數(shù)字的絕對值翠勉,我們可以將此函數(shù)傳入map
函數(shù)。還記得高階函數(shù)可以接受其它函數(shù)作為其參數(shù)嗎霉颠?
let values = [1, 2, 3, -4, 5];
const updateListMap = (values) => values.map(Math.abs);
updateListMap(values); // [1, 2, 3, 4, 5]
是不是簡潔对碌、優(yōu)雅很多?
Reduce
接受一個函數(shù)和一個集合蒿偎,將其組合為一個新的值朽们。
一個常見的例子是,獲取一個訂單的總額诉位。假設(shè)我們在某個電商網(wǎng)站上骑脱,將Product 1
、Product 2
苍糠、Product 3
和Product 4
加入了購物車(訂單)叁丧,我們想要計算購物車商品的總金額。
一種命令式的實現(xiàn)方式是岳瞭,遍歷商品列表拥娄,累加商品的金額。
var orders = [
{ productTitle: "Product 1", amount: 10 },
{ productTitle: "Product 2", amount: 30 },
{ productTitle: "Product 3", amount: 20 },
{ productTitle: "Product 4", amount: 60 }
];
var totalAmount = 0;
for (var i = 0; i < orders.length; i++) {
totalAmount += orders[i].amount;
}
console.log(totalAmount); // 120
使用reduce
函數(shù)瞳筏,我們可以構(gòu)建一個用來amount sum
的函數(shù)稚瘾,并將其傳給reduce
函數(shù)。
let shoppingCart = [
{ productTitle: "Product 1", amount: 10 },
{ productTitle: "Product 2", amount: 30 },
{ productTitle: "Product 3", amount: 20 },
{ productTitle: "Product 4", amount: 60 }
];
const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount;
const getTotalAmount = (shoppingCart) => shoppingCart.reduce(sumAmount, 0);
getTotalAmount(shoppingCart); // 120
此處姚炕,我們有購物車商品列表shoppingCart
摊欠,函數(shù)sumAmount
接受當(dāng)前總額currentTotalAmount
和訂單order
對象以執(zhí)行計算丢烘。
getTotalAmount
函數(shù)使用sumAmount
和起始值0對shoppingCart
做reduce
。
另外一種獲取總額的方式是組合map
和reduce
函數(shù)凄硼。什么意思呢铅协?我們可以首先使用map
將shoppingCart
轉(zhuǎn)換為amount
的數(shù)組,然后再使用sumAmount
函數(shù)做reduce
摊沉。
const getAmount = (order) => order.amount;
const sumAmount = (acc, amount) => acc + amount;
function getTotalAmount(shoppingCart) {
return shoppingCart
.map(getAmount)
.reduce(sumAmount, 0);
}
getTotalAmount(shoppingCart); // 120
getAmount
函數(shù)接受訂單對象order
并返回金額amount
。此處痒给,我們有[10, 30, 30, 60]
说墨。然后reduce
累加所有的值以得到結(jié)果。漂亮苍柏!
我們再來看一個將上面講解的幾個高階函數(shù)組合應(yīng)用的例子尼斧。
依然以購物車shopping cart
為例,假設(shè)購物車中有以下商品:
let shoppingCart = [
{ productTitle: "Functional Programming", type: "books", amount: 10 },
{ productTitle: "Kindle", type: "eletronics", amount: 30 },
{ productTitle: "Shoes", type: "fashion", amount: 20 },
{ productTitle: "Clean Code", type: "books", amount: 60 }
]
我們希望獲得購物車中所有書籍的總額试吁。如何做呢棺棵?
- 篩選出所有的書籍(filter)
- 將商品列表轉(zhuǎn)換為金額的列表(map)
- 將所有值累加(reduce)
let shoppingCart = [
{ productTitle: "Functional Programming", type: "books", amount: 10 },
{ productTitle: "Kindle", type: "eletronics", amount: 30 },
{ productTitle: "Shoes", type: "fashion", amount: 20 },
{ productTitle: "Clean Code", type: "books", amount: 60 }
]
const byBooks = (order) => order.type == "books";
const getAmount = (order) => order.amount;
const sumAmount = (acc, amount) => acc + amount;
function getTotalAmount(shoppingCart) {
return shoppingCart
.filter(byBooks)
.map(getAmount)
.reduce(sumAmount, 0);
}
getTotalAmount(shoppingCart); // 70