JavaScript函數式編程(純函數、柯里化以及組合函數)
- 2022 年 2 月 20 日
- 筆記
- javascript, js相關, 前端
JavaScript函數式編程(純函數、柯里化以及組合函數)
前言
函數式編程(Functional Programming),又稱為泛函編程,是一種編程範式。早在很久以前就提出了函數式編程這個概念了,而後面一直長期被面向對象編程所統治著,最近幾年函數式編程又回到了大家的視野中,JavaScript是一門以函數為第一公民的語言,必定是支援這一種編程範式的,下面就來談談JavaScript函數式編程中的核心概念純函數、柯里化以及組合函數。
1.純函數
1.1.純函數的概念
對於純函數的定義,維基百科中是這樣描述的:在程式設計中,若函數符合以下條件,那麼這個函數被稱之為純函數。
- 此函數在相同的輸入值時,需產生相同的輸出;
- 函數的輸入和輸出值以外的其他隱藏資訊或狀態無關,也和由I/O設備產生的外部輸出無關;
- 該函數不能有語義上可觀察的函數副作用,諸如「觸發事件」,使輸出設備輸出,或更改輸出值以外物件的內容等;
對以上描述總結就是:
- 對於相同的輸入,永遠會得到相同的輸出;
- 在函數的執行過程中,沒有任何可觀察的副作用;
- 同時也不依賴外部環境的狀態;
1.2.副作用
上面提到了一個詞叫「副作用」,那麼什麼是副作用呢?
- 通常我們所說的副作用大多數是指葯會產生的副作用;
- 而在電腦科學中,副作用指在執行一個函數時,除了得到函數的返回值以外,還在函數調用時產生了附加的影響,比如修改了全局變數的狀態,修改了傳入的參數或得到了其它的輸出內容等;
1.3.純函數案例
-
編寫一個求和的函數sum,只要我們輸入了固定的值,sum函數就會給我們返回固定的結果,且不會產生任何副作用。
function sum(a, b) { return a + b } const res = sum(10, 20) console.log(res) // 30
-
以下的sum函數雖然對於固定的輸入也會返回固定的輸出,但是函數內部修改了全局變數message,就認定為產生了副作用,不屬於純函數。
let message = 'hello' function sum(a, b) { message = 'hi' return a + b }
-
在JavaScript中也提供了許多的內置方法,有些是純函數,有些則不是。像操作數組的兩個方法slice和splice。
-
slice方法就是一個純函數,因為對於同一個數組固定的輸入可以得到固定的輸出,且沒有任何副作用;
const nums = [1, 2, 3, 4, 5] const newNums = nums.slice(1, 3) console.log(newNums) // [2, 3] console.log(nums) // [ 1, 2, 3, 4, 5 ]
-
splice方法不是一個純函數,因為它改變了原數組nums;
const nums = [1, 2, 3, 4, 5] const newNums = nums.splice(1, 3) console.log(newNums) // [ 2, 3, 4 ] console.log(nums) // [ 1, 5 ]
-
2.柯里化
2.1.柯里化的概念
對於柯里化的定義,維基百科中是這樣解釋的:
- 柯里化是指把接收多個參數的函數,變成接收一個單一參數(最初函數的第一個參數)的函數,並且返回接收餘下的參數,而且返回結果的新函數的技術;
- 柯里化聲稱「如果你固定某些參數,你將得到接受餘下參數的一個函數」;
總結:只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩餘的參數的過程就稱之為柯里化。
2.2.函數柯里化的過程
編寫一個普通的三值求和函數:
function sum(x, y, z) {
return x + y + z
}
const res = sum(10, 20, 30)
console.log(res) // 60
將以上求和函數柯里化得:
- 將傳入的三個參數進行拆解,依次返回一個函數,並傳入一個參數;
- 在保證同樣功能的同時,其調用方式卻發生了變化;
- 注意:在拆解參數時,不一定非要將參數拆成一個個的,也可以拆成2+1或1+2;
function sum(x) {
return function(y) {
return function(z) {
return x + y + z
}
}
}
const res = sum(10)(20)(30)
console.log(res)
使用ES6箭頭函數簡寫為:
const sum = x => y => z => x + y + z
2.3.函數柯里化的特點及應用
-
讓函數的職責更加單一。柯里化可以實現讓一個函數處理的問題儘可能的單一,而不是將一大堆邏輯交給一個函數來處理。
-
將上面的三值求和函數增加一個需求,在計算結果之前給每個值加上2,先看看不使用柯里化的實現效果:
function sum(x, y, z) { x = x + 2 y = y + 2 z = z + 2 return x + y + z }
-
柯里化的實現效果:
function sum(x) { x = x + 2 return function(y) { y = y + 2 return function(z) { z = z + 2 return x + y + z } } }
-
很明顯函數柯里化後,讓我們對每個參數的處理更加單一
-
-
提高函數參數邏輯復用。同樣使用上面的求和函數,增加另一個需求,固定第一個參數的值為10,直接看柯里化的實現效果吧,後續函數調用時第一個參數值都為10的話,就可以直接調用sum10函數了。
function sum(x) { return function(y) { return function(z) { return x + y + z } } } const sum10 = sum(10) // 指定第一個參數值為10的函數 const res = sum10(20)(30) console.log(res) // 60
2.4.自動柯里化函數的實現
function autoCurrying(fn) {
// 1.拿到當前需要柯里化函數的參數個數
const fnLen = fn.length
// 2.定義一個柯里化之後的函數
function curried_1(...args1) {
// 2.1拿到當前傳入參數的個數
const argsLen = args1.length
// 2.1.將當前傳入參數個數和fn需要的參數個數進行比較
if (argsLen >= fnLen) {
// 如果當前傳入的參數個數已經大於等於fn需要的參數個數
// 直接執行fn,並在執行時綁定this,並將對應的參數數組傳入
return fn.apply(this, args1)
} else {
// 如果傳入的參數不夠,說明需要繼續返回函數來接收參數
function curried_2(...args2) {
// 將參數進行合併,遞歸調用curried_1,直到參數達到fn需要的參數個數
return curried_1.apply(this, [...args1, ...args2])
}
// 返回繼續接收參數函數
return curried_2
}
}
// 3.將柯里化的函數返回
return curried_1
}
測試:
function sum(x, y, z) {
return x + y + z
}
const curryingSum = autoCurrying(sum)
const res1 = curryingSum(10)(20)(30)
const res2 = curryingSum(10, 20)(30)
const res3 = curryingSum(10)(20, 30)
const res4 = curryingSum(10, 20, 30)
console.log(res1) // 60
console.log(res2) // 60
console.log(res3) // 60
console.log(res4) // 60
3.組合函數
組合函數(Compose Function)是在JavaScript開發過程中一種對函數的使用技巧、模式。對某一個數據進行函數調用,執行兩個函數,這兩個函數需要依次執行,所以需要將這兩個函數組合起來,自動依次調用,而這個過程就叫做函數的組合,組合形成的函數就叫做組合函數。
需求:對一個數字先進行乘法運算,再進行平方運算。
-
一般情況下,需要先定義兩個函數,然後再對其依次調用:
function double(num) { return num * 2 } function square(num) { return num ** 2 } const duobleResult = double(10) const squareResult = square(duobleResult) console.log(squareResult) // 400
-
實現一個組合函數,將duoble和square兩個函數組合起來:
function composeFn(fn1, fn2) { return function(num) { return fn2(fn1(num)) } } const execFn = composeFn(double, square) const res = execFn(10) console.log(res) // 400
實現一個自動組合函數的函數:
function autoComposeFn(...fns) {
// 1.拿到需要組合的函數個數
const fnsLen = fns.length
// 2.對傳入的函數進行邊界判斷,所有參數必須為函數
for (let i = 0; i < fnsLen; i++) {
if (typeof fns[i] !== 'function') {
throw TypeError('The argument passed must be a function.')
}
}
// 3.定義一個組合之後的函數
function composeFn(...args) {
// 3.1.拿到第一個函數的返回值
let result = fns[0].apply(this, args)
// 3.1.判斷傳入的函數個數
if (fnsLen === 1) {
// 如果傳入的函數個數為一個,直接將結果返回
return result
} else {
// 如果傳入的函數個數 >= 2
// 依次將函數取出進行調用,將上一個函數的返回值作為參數傳給下一個函數
// 從第二個函數開始遍歷
for (let i = 1; i < fnsLen; i++) {
result = fns[i].call(this, result)
}
// 將結果返回
return result
}
}
// 4.將組合之後的函數返回
return composeFn
}
測試:
function double(num) {
return num * 2
}
function square(num) {
return num ** 2
}
const composeFn = autoComposeFn(double, square)
const res = composeFn(10)
console.log(res) // 400