函數(shù)式編程是一種編寫代碼的方式,而不是一種框架或工具,函數(shù)式的思維方式與面向?qū)ο蠹懊嫦蜻^程的思維方式完全不同。在學(xué)習(xí)如何使用函數(shù)式思考之前,首先必須知道函數(shù)式編程是什么。
什么是函數(shù)式編程
簡單來說,函數(shù)式編程是一種強(qiáng)調(diào)以函數(shù)使用為主的軟件開發(fā)風(fēng)格。其目標(biāo)是使用函數(shù)來抽象作用在數(shù)據(jù)之上的數(shù)據(jù)流與操作,從而在系統(tǒng)中消除副作用并減少對狀態(tài)的改變。
比如我們要在Web頁面中顯示一個(gè)"Hello, Jack!"為例講起:
document.querySelector('#msg').innerText = 'Hello, Jack!';
這段代碼非常簡單,因?yàn)檫@是寫死的??紤]下,類似這樣的功能有多個(gè)地方要用,并且有的地方要顯示"Hi,Jack!"。再多考慮一點(diǎn),如果我不僅要把文字輸是頁面中,比如控制臺呢?
現(xiàn)在比較明確的是有兩個(gè)邏輯:
- 對文字進(jìn)行定制,顯示"Hello, Jack!",還是顯示"Hi,Jack!"
- 將文字輸出到什么地方,頁面還是控制臺
為了復(fù)用性,我們可以將每一個(gè)邏輯都抽取成對應(yīng)的函數(shù):
這里只是為了演示而已,實(shí)際項(xiàng)目中是否有必要抽取要根據(jù)實(shí)際情況
// 用來生成"Hello, Jack!"的函數(shù)
function sayHello(name) { return `Hello, ${name}!`;}
// 用來生成"Hi,Jack!"的函數(shù)
function sayHi(name) {return `Hi, ${name}!`;}
// 將文本輸出到頁面中的函數(shù)
function printToPage(msg) {document.querySelector('#msg').innerText=msg;}
// 將文本輸出到控制臺中的函數(shù)
function printToConsole(msg){console.log(msg);}
至此,如果我們想要在頁面中輸出"Hi, Jack!",就可以像下面這樣:
printToPage(sayHi('Jack'))
觀察調(diào)用的兩個(gè)函數(shù),可以發(fā)現(xiàn)sayHi接收一個(gè)參數(shù),然后將返回的值傳給printToPage,那是否可以組合成一個(gè)更簡單的函數(shù)使用呢?
// 組合函數(shù)
function run(func1, func2) {
return params => {
func1(func2(params));
}
}
// 使用組合函數(shù)生成新函數(shù)
const printNameToPage = run(printToPage, sayHi);
// 運(yùn)行函數(shù)
printNameToPage('Jack');
這個(gè)時(shí)候如果想要一個(gè)輸出到控制臺的函數(shù):
// 使用組合函數(shù)生成新函數(shù)
const printNameToConsole = run(printToConsole, sayHello);
// 運(yùn)行函數(shù)
printNameToConsole('Jack'); // Hello, Jack!
以上就是一個(gè)簡單的函數(shù)思維的過程。也許你覺得沒有必要搞那么復(fù)雜,但是你想,真實(shí)項(xiàng)目中,你可是創(chuàng)建了多個(gè)可以重復(fù)使用的函數(shù): sayHi, sayHello, printToPage, printToConsole等。實(shí)際項(xiàng)目中,這樣的函數(shù)會非常的有價(jià)值。
為了充分理解函數(shù)式編程,我們必須知道它所基于的一些基本概念:
- 聲明式編程。
- 純函數(shù)。
- 引用透明。
- 不可變性。
下面對這些基本概念做簡單的解釋
函數(shù)式編程是聲明式編程
函數(shù)式編程屬于聲明式編程范式:這種范式會描述一系列的操作,但并不會暴露它們是如何實(shí)現(xiàn)的以及數(shù)據(jù)流是如何穿過它們的。
目前,更加主流的是命令式或者過程式的編程范式,其將計(jì)算程序視為自上而下的斷言,通過修改系統(tǒng)的各個(gè)狀態(tài)來計(jì)算最終結(jié)果。
先看一個(gè)命令式的例子。假如我們要計(jì)算一個(gè)數(shù)組中的所有數(shù)的平方,并得到一二新的數(shù)組,大概程序如下步驟:
let array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let newArray = [];
for (let i = 0, len = array.length; i < len; i++) {
newArray[i] = Math.pow(array[i], 2);
}
console.log(newArray); // [0, 1, 4, 9, 16, 25, 49, 64, 81]
命令式編程很具體的告訴計(jì)算機(jī)如何執(zhí)行某個(gè)任務(wù),如上的通過數(shù)組循環(huán)并對每一個(gè)元素應(yīng)用公式。
而聲明式編程是將程序的描述與求值分開的,它關(guān)注于如何用各種表達(dá)式來描述程序邏輯。下面使用函數(shù)式來解決相同的問題,只需要對應(yīng)用在每個(gè)元素上的行為給予關(guān)注,將循環(huán)交給系統(tǒng)的其他部分去控制:
let newArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(num => Math.pow(num, 2));
console.log(newArray); // [0, 1, 4, 9, 16, 25, 49, 64, 81]
為什么要去掉代碼循環(huán)?循環(huán)是一種重要的命令控制結(jié)構(gòu),但很難重用,并且很難插入其他操作中。函數(shù)式編程旨在盡可能地提高代碼的無狀態(tài)性和不變性。無狀態(tài)的代碼不會改變或破壞全局的狀態(tài),這就是純函數(shù)。
純函數(shù)
函數(shù)式編程基于一個(gè)前提,即使用純函數(shù)構(gòu)建具有不變性的程序
純函數(shù)具有以下性質(zhì):
- 僅取決于提供的輸入,而不依賴于任何在函數(shù)求值期間或調(diào)用間隔時(shí)可能變化的隱藏狀態(tài)和外部狀態(tài)。
- 不會造成超出其作用域的變化,例如修改全局對象或引用傳遞的參數(shù)。
任何不符合以上條件的函數(shù)都是"不純的",例如:
let counter = 0;
// 此函數(shù)是不純的,因?yàn)樽x取并且修改了一個(gè)外部變量
function increment() {return ++counter;}
// Date.now()也是不純的,因?yàn)樗蕾囉谝粋€(gè)不斷變化的因素: 時(shí)間
Date.now();
函數(shù)式編程在實(shí)踐上并不限制一切的改變,它只是提供了一個(gè)方式來幫助管理和減少可變狀態(tài),讓你能夠?qū)⒓兒瘮?shù)從不純的部分抽取出來。
引用透明
引用透明是定義一個(gè)純函數(shù)較為正確的方式。純度表明一個(gè)函數(shù)的參數(shù)和它返回值之間映射的純的關(guān)系。如果一個(gè)函數(shù)對于相同的輸入始終產(chǎn)生相同的結(jié)果,那么就是引用透明的。
上面的increment就不是引用透明的,因?yàn)樗蕾囃獠孔兞縞ounter。我們將其修改成引用透明的函數(shù):
// 不再依賴外部變量,也不會影響外部變量
function increment(counter) {return counter + 1;}
使用之前定義的run函數(shù),可以很簡單的組合出來一個(gè)加2的函數(shù):
const plus2 = run(increment, increment);
console.log(plus2(0)); // 2
存儲不可變數(shù)據(jù)
不可變數(shù)據(jù)是指那些被創(chuàng)建后不能更改的數(shù)據(jù)。
Javascript中所有的基本類型都是不可變的。但是其他對象,如數(shù)組,都是可變的??紤]一個(gè)簡單的數(shù)組排序:
function (arr) {
return arr.sort((a, b) => b - a);
}
這個(gè)函數(shù)的確能返回排序后的數(shù)組,但不幸的是,array.sort函數(shù)是有狀態(tài)的,原本的函數(shù)也被修改了。
基于以上的的描述,我們可以簡潔的理解函數(shù)式編程: 函數(shù)式編程是指為創(chuàng)建不可變程序,通過消除外部可見的副作用,來對純函數(shù)的聲明式的求值過程。
總結(jié)
- 使用純函數(shù)的代碼絕不會更改或破壞全局狀態(tài),有助于提高代碼的可測試性和可維護(hù)性。
- 函數(shù)式編程采用聲明式的風(fēng)格,易于推理。這提高了應(yīng)用程序的整體可讀性,通過使用組合和lamada表達(dá)式使代碼更加精簡。
- 集合中的數(shù)據(jù)元素處理可以通過鏈?zhǔn)饺鏼ap和reduce這樣的函數(shù)來實(shí)現(xiàn)。
- 函數(shù)式編程將函數(shù)視為積木,通過一等高階函數(shù)來提高代碼的模塊化和可重用性。
- 可以利用響應(yīng)式編程組合各個(gè)函數(shù)來降低事件驅(qū)動(dòng)程序的復(fù)雜性。