用函數(shù)編程 != 函數(shù)式編程
幾乎所有編程語言中我們都會用到函數(shù),但大部分情況都不是函數(shù)編程辈讶。函數(shù)編程的重要特點之一是使用函數(shù)的聲明式推導
來代替結構關鍵字的命令式過程
。比如
const obj = {...}
const list = [...]
//命令式
for(let v of obj){...}
for(let k in obj){...}
for(let i=list.length;i--;){...}
while(...){...}
//聲明式
each(obj, (v,k)=>...)
each(list, (v,i)=>...)
這個例子很好的詮釋了兩種編程模式的區(qū)別 —— 聲明式只關注輸入/輸出蝎困,不在乎循環(huán)是for...of
還是for...in
又或者for還是while录语,相反,命令式則關注執(zhí)行細節(jié)禾乘,因為不同的語法決定了循環(huán)子是value還是key钦无。
那么,如果在編碼中用了each就是函數(shù)編程了盖袭?某種意義上是的失暂,但函數(shù)編程不僅僅是用each函數(shù)代替for關鍵字,它包含很多概念 —— 純函數(shù)鳄虱、高階函數(shù)弟塞、可推導性、柯里化拙已、惰性計算等等决记,它是一種編程范式,是一種編碼習慣倍踪,更是一種思維模式系宫。關于什么是函數(shù)編程不是本文重點索昂,這里推薦一本很好的入門書 —— Luis Atencio的《JavaScript函數(shù)式編程指南》。
為什么要用函數(shù)式編程扩借?
難道命令式不能實現(xiàn)功能嗎椒惨?函數(shù)隱藏了細節(jié),錯了怎么辦潮罪?用了那么多函數(shù)還不叫函數(shù)式編程康谆?在具體舉例描述函數(shù)式編程的優(yōu)點前,我們先來回顧一下軟件的一些常見定義 —— 可讀性嫉到、健壯性沃暗、可維護性、運行效率何恶、封裝度孽锥。
下面通過一些編碼中常見的例子來對比函數(shù)式與命令式的區(qū)別:
場景1 —— 可讀性
- 當我們要判斷一個集合是否為空
//命令式
if(list.length < 1){
...
}
//聲明式
if(isEmpty(list)){
...
}
顯然從可讀性來看聲明式更好,況且命令式為了確保代碼運行正常還需要驗證list變量本身必須是Array類型细层,否則獲取.length屬性將收到一個錯誤惜辑,它看起來就像這樣
//命令式
if(list != null && list.length < 1){
...
}
- 輸入框字段不能為空
//命令式
if(name != null && name != undefined && name != ''){
...
}
//聲明式
if(!isBlank(name)){
...
}
例子中的函數(shù)不僅在可讀性上更勝一籌,同時也解決了代碼容錯問題今艺,我們不關心入?yún)⑹欠穹虾瘮?shù)內(nèi)部邏輯的邏輯韵丑,是否是一個有效值爵卒,也不用擔心無效值會導致程序異常虚缎,這是函數(shù)式編程中的一個概念 —— 透明引用 —— 如果 f(x)=y 那么 f(!x)=!y。
場景2 —— 健壯性
健壯性是函數(shù)式編程最重要的優(yōu)勢钓株,可以減少大量的潛在錯誤实牡,包括無效引用、類型異常轴合、范圍溢出等等创坞。
- 容錯處理
//不健壯
if(a.b == null){...}
list.forEach(...)
ary.splice(...)
obj['key']()
let x = list[5]
//健壯
if(isNull(get(a,'b'))){...}
each(list,...)
splice(ary,...)
get(obj,'key',()=>{})()
通過函數(shù)組合及默認值設置可以避免這些潛在錯誤。
- 錯誤調(diào)試Debug
恭喜受葛,函數(shù)式編程中針對函數(shù)本身不存在調(diào)試問題题涨,也就是說函數(shù)庫提供的函數(shù)無需調(diào)試。函數(shù)式編程所依賴的函數(shù)庫中的函數(shù)在使用時可以看作宿主系統(tǒng)提供的原生函數(shù)总滩,例如c語言中的printf
纲堵,js中的console.log
,java中的Math.random
闰渔,python中的print
等等席函。原因是函數(shù)式編程中提供的函數(shù)大部分都是純函數(shù),純函數(shù)的特征就是確定的輸入一定有確定的輸出冈涧,具有明確的可推導性茂附。
下面這個函數(shù)isNumber
當參數(shù)類型已知時正蛙,返回值可推導而不會產(chǎn)生程序異常或任何非boolean類型的結果营曼。
//返回值一定是boolean類型
const rs = isNumber(null) //true | false
當然乒验,在函數(shù)鏈模式下數(shù)據(jù)流本身是需要跟蹤調(diào)試的,比如
_([1,2,3,4])
.map(v=>v*3)
.tap(v=>console.log(v))//調(diào)試數(shù)據(jù)流
.join('-')
.value()
場景3 —— 可維護性
當我們開始維護代碼邏輯時溶推,最重要的當然是能看得通徊件,理得順。除了可讀性要高之外蒜危,邏輯間的可推導性也非常重要虱痕。比如,每個函數(shù)的返回值就是看上去應該返回的那個值辐赞。
- 字符串首字母大寫
//自定義
function cap(str){
//獲取str首字母及后續(xù)字符串
//把首字母變大寫
//返回大寫后的首字母 + 后續(xù)字符串
}
const str = cap('abc') //Abc | Error | ...
//使用函數(shù)庫
const str = capitalize('abc') //Abc
這是個很簡單的邏輯實現(xiàn)部翘,但區(qū)別是capitalize
函數(shù)的返回值一定是字符串,而cap
函數(shù)的返回值有以下幾種可能
- 正確字符串
- 異常字符串
- 錯誤中斷
比如响委,如果參數(shù)是null
新思,就無法獲取首字母及變大寫,這些操作最終會導致Error而中斷赘风,這就是不可推導性夹囚。整個邏輯會因為某些函數(shù)的不可預測而產(chǎn)生不穩(wěn)定性,但對于capitalize
函數(shù)邀窃,它的返回值永遠可預測荸哟。
- 獲取集合大小
//命令式
Object.keys(obj)
ary.length
set.size
//函數(shù)式
size(obj)
size(ary)
size(set)
統(tǒng)一的API有助于快速理解代碼意圖,良好的函數(shù)封裝可以屏蔽不同原生數(shù)據(jù)集合間的差異瞬捕,獲取size則是集合最常用的操作之一鞍历。
場景4 —— 封裝、重用
封裝是函數(shù)的基本特性肪虎,我們在編碼時創(chuàng)建自定義函數(shù)也是為了進行過程封裝及重用劣砍。函數(shù)式編程通常都會依賴于一個強大且豐富的函數(shù)庫(函數(shù)語言內(nèi)置函數(shù)庫,而js可以通過使用外置函數(shù)庫)且在不同的方面提供了多種封裝(比如本文開篇介紹的each
函數(shù))扇救。
- 獲取集合最后一個元素
//未封裝
const lastOne = list[list.length-1]
//已封裝
const lastOne = last(list)
- 集合映射 1 ? 1
//未封裝
const newList = [];
list.forEach(v=>newList.push(v+1))
//已封裝
const newList = map(list,v=>v+1)
- 集合映射 m ? n
//未封裝
const newList = [];
list.forEach(v=>if(n%2)newList.push(v+1))
//已封裝
const newList = flatMap(list,n=>n%2?n:[])
- 數(shù)據(jù)分組
const users = [
{name:'zhangsan',sex:'m',age:33},
{name:'lisi',sex:'f',age:21},
{name:'wangwu',sex:'m',age:25},
{name:'zhaoliu',sex:'m',age:44},
]
//未封裝
const grouped = {}
each(users, u=>{
if(!grouped[u.sex])grouped[u.sex] = []
grouped[u.sex].push(u)
})
//已封裝
const grouped = groupBy(users, u=>u.sex)
更多封裝的js函數(shù)庫可以查看這里;
場景5 —— 性能
針對js來說刑枝,如果做web開發(fā)數(shù)據(jù)量一大就會導致UI卡頓而影響使用體驗,尤其是大數(shù)據(jù)量的內(nèi)容處理比如嵌套循環(huán)或者大量數(shù)據(jù)進行分組/映射時可能會出現(xiàn)性能瓶頸迅腔。在函數(shù)式編程中可通過惰性計算及列表推導來解決大數(shù)據(jù)量集合處理装畅。
關于惰性計算及列表推導可以查看這里;關于集合操作性能優(yōu)化可以查看這里
結語
函數(shù)式編程已經(jīng)越來越多的影響到各種語言,即使是OOP大佬的JAVA也引入了stream API來處理集合數(shù)據(jù)钾挟,而對家C#也是更早引入了lambda洁灵。函數(shù)式編程并不是要放棄OOP,他們的關注點不同(但明顯js的函數(shù)性更純一些)。
學習并習慣函數(shù)式編程是一個漸進的過程徽千,不妨從上面列舉的幾個場景開始苫费,逐步了解掌握函數(shù)式編程。