函數(shù)式編程(functional programming
)或稱函數(shù)程序設(shè)計(jì),又稱泛函編程,是一種編程范型,比起命令式編程,函數(shù)式編程更加強(qiáng)調(diào)程序執(zhí)行的結(jié)果而非執(zhí)行的過程,倡導(dǎo)利用若干簡單的執(zhí)行單元讓計(jì)算結(jié)果不斷漸進(jìn),逐層推導(dǎo)復(fù)雜的運(yùn)算,而不是設(shè)計(jì)一個(gè)復(fù)雜的執(zhí)行過程。
函數(shù)式編程,近年來一直被炒得火熱,國內(nèi)外的開發(fā)者好像都在議論和提倡這種編程范式。在眾多的函數(shù)式語言中,Javascript
無疑是最亮眼的一個(gè),越來越多的人開始學(xué)習(xí)和擁抱它,并使用它運(yùn)用函數(shù)式編程來開發(fā)實(shí)際的大型應(yīng)用,開源社區(qū)也源源不斷的誕生函數(shù)式風(fēng)格的框架和類庫(
Angular / React / Redux
)。
作為 web
平臺唯一的標(biāo)準(zhǔn)通用語言,
Javascript
在軟件歷史上掀起了最大的語言熱潮,擁有當(dāng)下最大的開源包管理工具(
npm
)的
Javascript
也從
Lisp
手中接過了維持?jǐn)?shù)十年的
“
最流行的函數(shù)式編程語言
”
的名號。在
Javascript
的世界中是天然支持函數(shù)式編程的,函數(shù)式編程的基本特征有:
·
一等函數(shù)
·
閉包
·
高階函數(shù)
·
純度
本文會以 Javascript
為例子,和大家一起來了解和學(xué)習(xí)函數(shù)式編程。
一等函數(shù)(First Class Functions)
一等函數(shù)這個(gè)術(shù)語最早在20
世紀(jì)
60
年代,由英國計(jì)算機(jī)科學(xué)家 Christopher Strachey
在 functions as first-class citizens
一文中提出的。意思是指,函數(shù)和其他一等公民(Number / String...)
一樣,擁有和它們一樣的能力和作用:
·
函數(shù)儲存為變量
const foo = () => {...}
·
函數(shù)可以儲存為數(shù)據(jù)的一個(gè)元素
const arr = [1, 2, () => {...}]
·
函數(shù)可以作為對象的屬性值
const obj = {name: 'xx', say: () => {}}
·
函數(shù)可以在使用時(shí)直接創(chuàng)建出來
1 + (() => { return 2; })()
·
函數(shù)可以作為變量傳遞給另一個(gè)函數(shù)
bar (name, fun) { fun(name) }
bar('xx', (name) => { console.log(name) })
·
函數(shù)可以被另一個(gè)函數(shù)返回
foo() {
return () => {...}
}
在函數(shù)式編程中,函數(shù)是作為基本單元,并且在函數(shù)之上建立代碼和數(shù)據(jù)的封裝,以提高應(yīng)用的重用和靈活性。支持一等函數(shù)的作用是顯而易見的,我們可以使用函數(shù)去完成大部分的功能。
閉包(Closure)
歷經(jīng)了 30
年,閉包終于成為了編程語言的主要特點(diǎn)。但是根據(jù)一項(xiàng)調(diào)查顯示,有關(guān)
Javascript
閉包的問題占了
23%
左右,對于相當(dāng)數(shù)量的開發(fā)者來說閉包仍然模糊而又神秘。對于閉包解釋我還是更傾向于 Kyle Simpson
的系列書 You Don’t Know JavaScript
中的解釋:
函數(shù)在被定義時(shí)是可以訪問當(dāng)前的詞法作用域,當(dāng)函數(shù)離開作用域之外被執(zhí)行時(shí),就形成了閉包。
簡而言之,閉包就是一個(gè)函數(shù),捕獲了作用域內(nèi)的外部綁定。來看個(gè)例子:
function student (people) {
return (name) => { return people[name] }
}var someone = student({xx: {age: 20}, jackson: {age: 21}})
someone('xx') // {age: 20}
在執(zhí)行完 student
函數(shù)后,里面的匿名函數(shù)形成了一個(gè)閉包,閉包是可以訪問到
people
對象。閉包為
Javascript
提供了私有訪問,這讓給開發(fā)者建立數(shù)據(jù)抽象提供了極大地便利,也可以更好地書寫函數(shù)式代碼,建立更加強(qiáng)大的代碼。
來思考一個(gè)場景,手頭上擁有一個(gè)書本的數(shù)組,數(shù)組里面包含了書本的信息,現(xiàn)在需要做的是找出把書名填充到一個(gè)數(shù)組中并且返回,我們一般都會這樣寫:
const books = [{title: '
人類簡史
', author: 'zz'}, {title: '
禪與摩托車維修藝術(shù)
books.map((item) => { return item.title })
我們使用了 Array.prototype.map
方法,傳入了一個(gè)匿名函數(shù),函數(shù)中
return
了書名
title
。假如需要利用閉包來進(jìn)一步抽象的話,要怎么寫呢?
function plucker (key) {
return (obj) => {
return (obj && obj[key])
}
}
books.map(plucker('title'))
我們定義了一個(gè) plucker
函數(shù),它接收一個(gè)
key
參數(shù)并返回一個(gè)匿名函數(shù),匿名函數(shù)就是一個(gè)閉包并補(bǔ)捕獲了
key
參數(shù)。在利用了閉包的情況下,我們可以傳入任意想要的書本信息(比如:
plucker('author')
),這樣就提高了代碼的重用性和靈活性。當(dāng)我們對于閉包認(rèn)識足夠充分時(shí)并合理運(yùn)用到實(shí)際開發(fā)中去,將會切身體會到閉包的威力和它給我們帶來的便利。
高階函數(shù)(Higher Order Functions)
在數(shù)學(xué)和計(jì)算機(jī)科學(xué)中,高階函數(shù)式至少滿足下列一個(gè)條件的函數(shù):
·
接受一個(gè)或多個(gè)函數(shù)作為輸入
·
輸出一個(gè)函數(shù)
在上述的 plucker
函數(shù)就是一個(gè)例子,還有我們熟知的
Array.prototype
相關(guān)的方法,比如
.map
、
.sort
等等都是高階函數(shù),因?yàn)樗鼈儩M足接受一個(gè)函數(shù)作為參數(shù)的條件。
那么先來看一個(gè)一階函數(shù)的例子,定義一個(gè)函數(shù),它會將數(shù)組中4
個(gè)字母的單詞給過濾掉:
const words = ['foo', 'bar', 'test', 'some']; const filter = words => {
let arr = [];
for(let i = 0, { length } = words; i < length; i++) {
const word = word;
if(word.length !== 4) {
arr.push(word);
}
}
return arr;
}
filter(words); // ['foo', 'bar']
假如現(xiàn)在又需要過濾數(shù)組中,以 ‘b’
字母開頭的單詞?那么再定義一個(gè)函數(shù):
const startWith = words => {
let arr = [];
for(let i = 0, { length } = arr; i < length; i++) {
const word = word;
if(word.indexOf('b') !== 0) {
arr.push(word);
}
}
return arr;
}
filter(words); // ['foo', 'test', 'some']
根據(jù)上面兩個(gè)函數(shù)的對比來看,其實(shí)主要代碼的邏輯都是相似的,先遍歷數(shù)組再進(jìn)行條件判斷,最后 push
到數(shù)組中。其實(shí),遍歷和過濾都可以抽象出來,可以方便其他的類似函數(shù)去調(diào)用,畢竟在數(shù)組中根據(jù)條件過濾是很常見的需求。
const reduce = (reducer, init, arr) => {
let acc = init;
for(let i = 0,{ length } = arr; i < length; i++) {
acc = reducer(acc, arr);
}
return acc;
}
reduce((acc, curr) => acc + curr, 0, [1, 2, 3]); // 6
如果使用過 Underscore
庫的話,就會發(fā)現(xiàn)
reduce
和
Underscore.reduce
作用是一樣的,實(shí)現(xiàn)的是累計(jì)的功能。
reduce
接受了
3
個(gè)參數(shù):
ruducer
函數(shù)、累計(jì)的初始值和一個(gè)數(shù)組,遍歷時(shí)將每個(gè)數(shù)組元素作為
reducer
的參數(shù)傳入,返回值又賦值給累計(jì)變量
init
,遍歷完成時(shí)也就完成了累計(jì)的功能。
現(xiàn)在如果將 rudece
應(yīng)用到第一個(gè)需求上(過濾四個(gè)字母的單詞):
const func = (fn ,arr) => {
return reduce((acc, curr) => fn(curr) ? acc.concat([curr]) : acc, [], arr)
}console.log(func(word => word.length !== 4, words)); // ["foo", "bar"]
可以發(fā)現(xiàn),將公共代碼抽象出來之后,filter
的函數(shù)實(shí)現(xiàn)非常簡潔,只需傳入不同的條件函數(shù),就能為我們?nèi)ヌ幚矸细鞣N條件的數(shù)據(jù)。高階函數(shù)可以用來實(shí)現(xiàn)函數(shù)的多態(tài)性,并且相對于一階函數(shù),高階函數(shù)的復(fù)用性和靈活性更好。
純度(Purity)
函數(shù)式編程不僅僅只關(guān)心函數(shù),也是思考如何盡量地降低軟件復(fù)雜性的一種方式。在一些函數(shù)式編程語言中,純度是被強(qiáng)制執(zhí)行的,不允許使用有副作用的表達(dá)式。但是在 Javascript
中,純度必須通過管理區(qū)實(shí)現(xiàn),并且非常容易在偶然間創(chuàng)建和使用非純函數(shù)。
一個(gè)純函數(shù)需要滿足以下三個(gè)條件:
·
函數(shù)結(jié)果只能通過參數(shù)來計(jì)算得出
·
不能依賴于能被外部操作改變的數(shù)據(jù)
·
不能改變外部狀態(tài)
根據(jù)這上述條件來看,在 Javascript
的世界中去維持絕對純凈是不可能的,因?yàn)槿鄙倭舜蠖鄶?shù)函數(shù)式語言中使用的高效、不變的數(shù)據(jù)結(jié)構(gòu)。我們知道在
Javascript
擁有能力去 freeze()
對象,但是只能對接對象的頂級屬性,這就意味著一個(gè)嵌套對象下的屬性是仍然能夠被更改的。
var obj = Object.freeze({
foo: 'hello',
bar: {
text: 'world'
}
})
obj.foo = 'goodbye';console.log(obj.foo); // hello
obj.bar.text = 'goobye';console.log(obj.bar.text); // goodbye
在 ES6
中新增的
const
關(guān)鍵字,使用
const
可以定義一個(gè)不能夠被重新賦值為不同的值,但是一個(gè)
const
對象的屬性還是可變的。
const obj = 'hello';
obj = 'goodbye'; // Uncaught TypeError: Assignment to constant variable.
const obj = {
foo: 'hello',
bar: 'world'
}
obj.foo = 'goodbye';
console.log(obj); // {foo: 'goodbye', bar: 'world'}
在 Javascrpt
中實(shí)現(xiàn)綜合不變性還有很長的路要走。換句話來說,雖然不能夠保證絕對的純凈,但是我們可以將純凈的部分抽離出來,將變化的影響降到最低,使得代碼變得更加通用和容易測試。
總結(jié):
·
函數(shù)式編程是支持一等函數(shù)的,函數(shù)具有其他數(shù)據(jù)類型相同的功能
·
函數(shù)式編程中使用閉包來進(jìn)行數(shù)據(jù)的封裝
·
使用高階函數(shù)來建立代碼的抽象,使代碼更加靈活通用
·
盡量抽離純函數(shù)來保持代碼的可測性和通用性
來源:
稀土掘金