寫在前面的話
之前想看函數(shù)式編程時找了許多資料瓮具,許多對這個只是大概的描述了一段驹闰,然后看到有人推薦這本書芹关,所以寫下這本讀書筆記,給有需要的人參考行瑞。
第一章 函數(shù)式編程簡介
函數(shù)的第一條原則是要小奸腺。函數(shù)的第二條原則是要更小。 ——ROBERT C.MARTIN
1.1 什么是函數(shù)式編程血久?
f(x) = Y
- 函數(shù)必須總是接受一個參數(shù)
- 函數(shù)必須總是返回一個值
- 函數(shù)應(yīng)該依據(jù)接收到的參數(shù)(X)而不是外部環(huán)境運行
- 對于一個給定的X突照,只會輸出唯一的一個Y
函數(shù)式編程技術(shù)主要基于數(shù)學(xué)函數(shù)
和它的思想
。<br />
看個栗子:
var percentValue = 5;
var caculateTax = (value) => {
return value/100 * (100 + percentValue)
}
這個caculateTax
依賴于全局變量percentValue
, 因此在數(shù)學(xué)意義上不能被稱作真正的函數(shù)氧吐。修改成:
var caculteTax = (value, percentValue) => {
return value/100 * (100 + percentValue)
}
現(xiàn)在caculateTax
算得上一個真正的函數(shù)了讹蘑,通過這個簡單的栗子,可以簡單定義一下函數(shù)式編程:函數(shù)式編程是一種范式筑舅,我們能夠一次創(chuàng)建僅依賴輸入就可以完成自身邏輯的函數(shù)座慰。
這保證了當(dāng)函數(shù)被多次調(diào)用時仍然返回相同的結(jié)果,不會改變?nèi)魏瓮獠凯h(huán)境的變量翠拣,這將產(chǎn)生可緩存版仔、可測試的代碼庫。
函數(shù)與javascript方法
函數(shù)
是一段可以通過其名稱被調(diào)用的代碼。它可以傳遞參數(shù)并返回值蛮粮。
var simple = (a) => { return a }
// 一個簡單的函數(shù)
simple(5)
// 用其名稱調(diào)用
然而方法
是一段必須通過其名稱及其關(guān)聯(lián)對象的名稱被調(diào)用的代碼背桐。
var obj = { simple : (a) =>{ return a } }
obj.simple(5)
// 用其名稱及其關(guān)聯(lián)對象調(diào)用
1.2 引用透明性
所有的函數(shù)對于相同的輸入都將返回相同的值,這一特性被稱為引用透明性蝉揍。
一個簡單的函數(shù)
var identity = (i) => { return i }
該函數(shù)滿足引用透明性,現(xiàn)在它被用于其它函數(shù)調(diào)用之間畦娄。
sum(4, 5) + identity(1)
根據(jù)引用透明性又沾,上面的語句可以轉(zhuǎn)換成
sum(4, 5) + 1
這個過程被稱為替換模型
, 因為可以直接替換函數(shù)的結(jié)果(主要是因為函數(shù)的邏輯不依賴其它全局變量),這與它的值是一樣的熙卡。這也使得并發(fā)代碼和緩存成為可能杖刷。
遵循引用透明性的函數(shù)只依賴來自參數(shù)的輸入, 不依賴全局數(shù)據(jù)。因此線程可以自由地運行驳癌,沒有任何鎖機制滑燃。
引用透明性是一種哲學(xué)
“引用透明性”一詞來自分析哲學(xué)。該哲學(xué)分支研究自然語言的語義及其含義颓鲜。單詞“Referential”或“Referent”意指表達式引用的事物表窘。句子中的上下文是“引用透明的”,如果用另一個引用相同實體的詞語替換上下文中的一個詞語甜滨,并不會改變句子的含義乐严。
替換函數(shù)的值并不影響上下文,這就是函數(shù)式編程的哲學(xué)衣摩!
1.3 命令式昂验、聲明式與抽象
函數(shù)式編程主張聲明式編程和編寫抽象的代碼。
來看個栗子:
// 打印數(shù)組的每個元素
var array = [1, 2, 3]
for (i = 0; i < array.length; i++)
console.log(array[i]) // 打印 1, 2, 3
用數(shù)組長度的索引計算結(jié)果編寫了一個隱式的for循環(huán)并打印出數(shù)組項艾扮,"打印數(shù)組的元素"既琴,但是看起來像是在告訴編譯器該做什么,在上面栗子中泡嘴,告訴編譯器“要獲得數(shù)組長度甫恩、循環(huán)數(shù)組、用索引獲取每一個數(shù)組元素酌予,等等”我們將之稱為“命令式
”解決方案填物。命令式編程主張告訴編譯器“如何”做。
現(xiàn)在來看另外一方面霎终,聲明式編程滞磺。在聲明式編程中,我們要告訴編譯器做“什么”莱褒,而不是“如何”做击困。“如何”做的部分將被抽象到普通函數(shù)中(這些函數(shù)被稱為高階函數(shù)),更改如下:
var array = [1, 2, 3]
array.forEach((element) => {
console.log(element) // 打印 1, 2, 3
})
這個代碼片段移除了如何做的部分阅茶,只關(guān)心做什么的部分蛛枚。
函數(shù)式編程主張以抽象的方式創(chuàng)建函數(shù),這些函數(shù)能夠在代碼的其他部分被重用脸哀。
1.4 純函數(shù)
大多數(shù)函數(shù)式編程的好處來自于編寫純函數(shù)蹦浦,那么什么是純函數(shù)?
純函數(shù):對給定的輸入返回相同的輸出的函數(shù)撞蜂。
var double = (value) => value * 2
這個"double"函數(shù)就是一個純函數(shù)盲镶,遵循引用透明性。
- 純函數(shù)產(chǎn)生可測試的代碼
不純的函數(shù)具有副作用蝌诡,例如之前的這個函數(shù)
var percentValue = 5;
var caculateTax = (value) => {
return value/100 * (100 + percentValue)
}
caculateTax
不是純函數(shù)溉贿,因為它依賴外部環(huán)境計算其邏輯。假設(shè)在運行相同的測試用例時浦旱,外部環(huán)境也在改變變量percentValue
的值宇色,那么之前給的定輸入對應(yīng)的就不是唯一的值了。
純函數(shù)有一個重要屬性:純函數(shù)不應(yīng)改變?nèi)魏瓮獠凯h(huán)境的變量颁湖。
換而言之宣蠕,純函數(shù)不應(yīng)依賴任何外部變量也不應(yīng)改變?nèi)魏瓮獠孔兞俊?/code>
// 舉個反例
bar global = 'globalValue'
var badFunction = (value) => {
global = 'changed'
return value * 2
}
看看這個反例,當(dāng)badFunction
被調(diào)用時甥捺,它將全局變量global
的值變更了植影,假設(shè)另一個函數(shù)的邏輯依賴global
變量的話,調(diào)用badFunction
就影響了其他函數(shù)的行為涎永。具有這種性質(zhì)的函數(shù)會使得代碼庫變得難以測試思币。
- 合理的代碼
通過創(chuàng)建和使用純函數(shù),能夠讓我們非常簡單的推理代碼或函數(shù)羡微。函數(shù)(無論它是否為純函數(shù))必須總是具有一個有意義的名稱谷饿。例如double
加倍函數(shù)就不能命名成dd
。
用一個內(nèi)置的
Math.max
函數(shù)來測試一下推理能力妈倔。
給定函數(shù)調(diào)用:
Math.max(3, 4, 5, 6)
為了給出結(jié)果博投,是不是沒有看max的實現(xiàn)?為什么盯蝴?因為Math.max是純函數(shù)毅哗。
1.5 并發(fā)代碼
純函數(shù)總是允許我們并發(fā)地執(zhí)行代碼,因為純函數(shù)不會改變它的環(huán)境捧挺。當(dāng)然虑绵,js并沒有真正的多線程來并發(fā)地執(zhí)行函數(shù),但如果你的項目使用了webworker來模擬多線程并行執(zhí)行任務(wù)闽烙,這種時候就需要用純函數(shù)來代替非純函數(shù)翅睛。
舉個栗子:
let global = "something"
let function1 = (input) => {
// 處理input
// 改變global
global = "somethingElse"
}
let function2 = () => {
if (global === "something") {
// 業(yè)務(wù)邏輯
}
}
如果我們需要并發(fā)的執(zhí)行function1
和 function2
,假設(shè)線程T-1選擇function1
執(zhí)行,線程T-2選擇function2
執(zhí)行捕发,如果T-1在T-2之前執(zhí)行疏旨,那么并發(fā)執(zhí)行這些函數(shù)就會引起不良反應(yīng),現(xiàn)在把它們改為純函數(shù)扎酷。
let function1 = (input, global) => {
// 處理input
// 改變global
global = "somethingElse"
}
let function2 = (global) => {
if (global === "somethins") {
// 業(yè)務(wù)邏輯
}
}
移動了global
變量檐涝,把它作為兩個函數(shù)的參數(shù),使它們變成純函數(shù)法挨。由于函數(shù)并不依賴外部環(huán)境(global 變量)谁榜,因此不必像之前那樣擔(dān)心線程的執(zhí)行順序。
1.6 可緩存
純函數(shù)總是為給定的輸入返回相同的輸出坷剧,所以緩存純函數(shù)的輸出也是可能的。
舉個栗子喊暖,有這么一個做耗時計算的函數(shù)惫企。
var longRunningFunction = (ip) => {
// do long running tasks and return
}
然后我們有一個記賬對象,它存儲了longRunningFunction
函數(shù)的所有調(diào)用結(jié)果陵叽。
var longRunningFnBookKeeper = {
2: 3,
4: 5,
...
}
使用純函數(shù)的定義狞尔,在調(diào)用longRunningFunction
之前檢查key
是否存在longRunningFnBookKeeper
中,比如說:
// 檢查key是否在longRunningFnBookKeeper中
// 如果在巩掺,則返回結(jié)果偏序,否則更新記賬對象
longRunningFnBookKeeper.hasOwnProperty(ip) ?
longRunningFnBookKeeper[ip] :
longRunningFnBookKeeper[ip] = longRunningFunction(ip)
這樣就是緩存了純函數(shù)的調(diào)用結(jié)果。
1.7 管道與組合
使用純函數(shù)胖替,我們只需要在函數(shù)中做一件事研儒。純函數(shù)應(yīng)該被設(shè)計為只做一件事。只做一件事并把它做到完美是UNIX的哲學(xué)独令,我們在實現(xiàn)純函數(shù)時也將遵循這一原則端朵。UNIX和LINUX平臺有很多用戶日常任務(wù)的命令,例如:cat
用于打印文件內(nèi)容燃箭,grep
用于搜索文件冲呢,wc
用于計算行數(shù)。這些命令的確一次只解決一個問題招狸,但是可以通過組合或者管道來完成復(fù)雜的任務(wù)敬拓。假如我們要在一個文件中找到一個特定的名稱并統(tǒng)計它出現(xiàn)次數(shù),在命令提示符中命令如下:
cat jsBook | grep -i "composing" | wc
組合不是UNIX/LINUX命令行獨有的裙戏,但它們是函數(shù)式編程范式的核心乘凸。稱為函數(shù)式組合。
javascript不支持組合函數(shù)函數(shù)的操作符"|"累榜,但是我們可以創(chuàng)建自己的支持組合的函數(shù)翰意。
1.8 純函數(shù)是數(shù)學(xué)函數(shù)
先見1.6 “可緩存”中的那段longRunningFunction
和longRunningFnBookKeeper
代碼。
假設(shè)通過多次調(diào)用,longRunningFnBookKeeper
增長為如下的對象:
longRunningFnBookKeeper = {
1: 32,
2: 4,
3: 5,
5: 6,
11: 44
}
分析一下該對象冀偶,可見longRunningFnBookKeeper
接受一個輸入并為給定的范圍映射輸出醒第,此處的關(guān)鍵是,輸入具有強制的、相應(yīng)的輸出。在key中也不存在映射兩個輸出的輸入重贺。
比對數(shù)學(xué)函數(shù)的定義(維基百科看不了)龙填,基本上與純函數(shù)一致,從上述例子也能看到數(shù)學(xué)函數(shù)的思想被借鑒到函數(shù)式范式的世界述么。
1.9 javascript是函數(shù)式編程語言嗎
javascript是函數(shù)式編程語言,函數(shù)式編程主張函數(shù)必須接受至少一個參數(shù)并返回一個值。坦率的說司恳,可以創(chuàng)建一個不接收參數(shù)并且實際上什么也不返回的函數(shù),例如這個:var useless = () => {}
绍傲,這串代碼執(zhí)行并不會報錯扔傅,原因是javascript不是一種純函數(shù)語言(比如說Haskell),而更像是一種多范式語言。
javascript支持將函數(shù)作為參數(shù)烫饼,以及將函數(shù)傳遞給另一函數(shù)等特性——主要原因是javascript將函數(shù)視為一等公民猎塞,由于函數(shù)定義的約束,開發(fā)者需要在創(chuàng)建javascript函數(shù)時將其考慮在內(nèi)杠纵。