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