純函數
函數式編程中的函數,指的就是純函數,這也是整個函數式編程的核心
純函數:相同的輸入永遠會得到相同的輸出,而且沒有任何可觀察的副作用。
純函數就類似數學中的函數(用來描述輸入和輸出之間的關系),y = f(x)
- lodash 是一個純函數的功能庫,提供了對數組、數字、對象、字符串、函數等操作的一些方法
來感受下啥叫純與不純
數組的 slice 和 splice 分別是:純函數和不純的函數
slice: 返回數組中的指定部分,不會改變原數組
splice: 對數組進行操作返回該數組,會改變原數組
let numbers = [1, 2, 3, 4, 5]
// 純函數
numbers.slice(0, 3)// => [1, 2, 3]
numbers.slice(0, 3)// => [1, 2, 3]
numbers.slice(0, 3)// => [1, 2, 3]
// 不純的函數
numbers.splice(0, 3)// => [1, 2, 3]
numbers.splice(0, 3)// => [4, 5]
numbers.splice(0, 3)// => []
可以看到每次都是相同的輸入,slice每次都是相同的輸出,所以他是純函數,由于splice會改變原數組,雖然相同輸入,每次輸出都變了,所以不純
- 函數式編程不會保留計算中間的結果,所以變量是不可變的(無狀態的)
- 我們可以把一個函數的執行結果交給另一個函數去處理
就比如這個slice這個純函數,我們在調用他的時候會傳遞參數,然后會獲取結果,而函數內部的結果我們是無法獲取到的,也就是他不會保留內部的計算中間的結果,所以我們認為函數式編程的變量是不可變的
所以在基于函數式編程的過程中,我么會經常需要一些細粒度的純函數,要是自己去寫細粒度的純函數,要寫非常多,并不方便,所以我們有好多函數式編程的庫,比如說Lodash,有了這些細粒度的函數,我們可以把一個函數的執行結果交給另一個函數去處理,我們就能組合出功能更強大函數
Lodash
去官網看看吧,都是些好用的方法,https://www.html.cn/doc/lodash/
純函數的好處
- 可緩存
因為純函數對相同的輸入始終有相同的結果,所以可以把純函數的結果緩存起來
假設有一個超復雜的計算的函數,比如要計算地球的體積,他的入參是地球半徑,每計算一次要耗時一秒,那么我們就可以把計算結果儲存起來,因為對相同的輸入始終有相同的結果,其中lodash里有這么個記憶函數memoize
const _ = require('lodash')
function getVolume (r) {
console.log("半徑是" + r)
return 4 / 3 * Math.PI * r * r *r
}
let getVolumeWithMemory = _.memoize(getVolume)
console.log(getVolumeWithMemory(10))
console.log(getVolumeWithMemory(10))
console.log(getVolumeWithMemory(10))
// 輸出:
// 半徑是10
// 4188.79
// 4188.79
// 4188.79
被memoize包裹后形成的getVolumeWithMemory,入參和getVolume是一樣的,可以看到半徑是10只執行了一遍,10=>4188.79這個結果就被存起來了,下次再遇到,就直接拿出結果了,不需要再執行了。
我們自己模擬一個memoize
function memoize (f) {
let cache = {}
// 這個cache就是用來緩存結果的,用f的入參當key,用出參當value,比如上面的getVolumeWithMemory執行完后就形成了{10 : 4188.79}
return function () {
let key = JSON.stringify(arguments) // arguments可能是個數組或其他形式,所以轉成字符串
cache[key] = cache[key] || f.apply(f, arguments) // cache[key]存在就取cache[key],不存在就執行f,因為arguments是數組所以用apply方法
return cache[key]
}
}
可測試
純函數讓測試更方便并行處理
在多線程環境下并行操作共享的內存數據很可能會出現意外情況
純函數不需要訪問共享的內存數據,所以在并行環境下可以任意運行純函數 (es6 新增的Web Worker,讓js有了多線程能力)
副作用
純函數:對于相同的輸入永遠會得到相同的輸出,而且沒有任何可觀察的副作用
// 不純的
let mini = 18
function checkAge (age) {
return age >= mini
}
//你看那個不純的,你敢保證每次輸入20都返回true么,因為它依賴了一個外部變量,無法知曉此變量何時會被篡改
// 純的(有硬編碼,后續可以通過柯里化解決)
function checkAge (age) {
let mini = 18
return age >= mini
}
副作用讓一個函數變的不純(如上例),純函數的根據相同的輸入返回相同的輸出,如果函數依賴于外部的狀態就無法保證輸出相同,就會帶來副作用。
副作用的來源除了一些全局變量,還有配置文件,數據庫,獲取用戶的輸入等等
所有的外部交互都有可能帶來副作用,副作用也使得方法通用性下降不適合擴展和可重用性,同時副作用會給程序中帶來安全隱患給程序帶來不確定性,比如用戶的賬號密碼是要存在數據庫而非函數里的,所以副作用不可能完全禁止,但要盡可能控制它們在可控范圍內發生。
柯里化
這里我們先上一個案例,用柯里化來解決上一個例子中硬編碼的問題
function checkAge (age) {
let mini = 18
return age >= mini
}
// 既然有硬編碼,那我們通過把min字段傳進去,不就解決了么,于是
// 普通純函數
function checkAge (age, min) {
return age >= min
}
checkAge(20, 18)
checkAge(24, 18)
checkAge(26, 30)
// 假設經常以18,30為基準值,每次都輸入18,30就過于重復了
// 想想我們之前在閉包那里是怎么處理的
// 柯里化
function checkAge (min) { //既然經常是基準值不變的,所以就讓基準值通過閉包儲存起來
return function (age) {
return age >= min
}
}
let checkAge18 = checkAge(18)
let checkAge30 = checkAge(30)
checkAge18(24) // 再判斷數字的時候就不用輸入18了,這就是函數柯里化
checkAge18(20)
// ES6 寫法
let checkAge = min => (age => age >= min)
柯里化:
- 當一個函數有多個參數的時候先傳遞一部分參數調用它(這部分參數以后永遠不變)
- 然后返回一個新的函數接收剩余的參數,返回結果
我們看看普通純函數變成柯里化函數的過程是不是就是如上定義一樣,還挺套路的
Lodash中的柯里化
既然如此套路,Lodash中也有通用的柯里化方法curry
_.curry(func)
- 功能:創建一個函數,該函數接收一個或多個 func 的參數,如果 func 所需要的參數都被提供則執行 func 并返回執行的結果。否則繼續返回該函數并等待接收剩余的參數。
- 參數:需要柯里化的函數
- 返回值:柯里化后的函數
const _ = require('lodash')
// 要柯里化的函數
function getSum (a, b, c) {
return a + b + c
}
// 柯里化后的函數
let curried = _.curry(getSum)
// 測試
curried(1, 2, 3)
curried(1)(2)(3)
curried(1, 2)(3) // 輸出結果都是6
柯里化讓多元函數轉變成了一元函數(幾個入參就是幾元函數,上面的getSum就是三元函數)
柯里化案例
用上述的curry方法來實現一個案例:提取一個字符串中的所有數字
// 面向過程的方式, 正則match
''.match(/\d+/g) // 數字
// 如果改成提取數組中含有數字的元素,上面的方法就不通用了,所以我們用柯里化來包裝一下
const _ = require('lodash')
let match = _.curry(function (reg, str) {
return str.match(reg)
})
// 讓他具有特定功能
let findStrNum = match(/\d+/g) //尋找數字
let findStrSpace = match(/\s+/g) //尋找空格
// 試一試
console.log(findStrNum('asd1234')) // true
console.log(findStrSpace('asd1234')) // false
// OK,到這里,關于字符串的match就都可以實現了
// 現在我們要繼續用上面方法,來實現數組中的提取含有數字的項
// 數組需要循環,我們來一個柯里化的filter
let filter = function (fn, arr) { // 這里的fn是要做操作的函數
return arr.filter(fn) // 這個filter是數組的filter方法,別搞混
}
let filterCurry = _.curry(filter) // 柯里化
// 讓他具有特定功能, 那么就可以傳進去上面定義的findStrNum
let findArrNum = filterCurry(findStrNum)
// 試一試
console.log(findArrNum(['abc123', 'abc']))
最后我們就用函數式的方式,實現了這個功能,可能覺得這樣寫非常麻煩,還不如自己去正則寫來實現,但是你要清楚的是,將來這些函數,我們可以不停的重復使用。
const _ = require('lodash')
let match = _.curry(function (reg, str) {
return str.match(reg)
})
let findStrNum = match(/\d+/g)
let findStrSpace = match(/\s+/g)
let filterCurry = _.curry(function (fn, arr) {
return arr.filter(fn)
})
let findArrNum = filterCurry(findStrNum)
let findArrSpace = filterCurry(findStrSpace)
看看上面這些東西,定義一次,就可以在你的工程中無數次的重復使用,能避免自己造輪子中的小bug
模擬 _.curry() 的實現
// 先來看一下這玩意當時是如何使用的
const _ = require('lodash')
function getSum (a, b, c) {
return a + b + c
}
let curried = _.curry(getSum)
curried(1, 2, 3) // 它的調用形式分為傳入全部參數,或部分參數
curried(1, 2)(3)
// 自己實現
function curry (func) {
return function curriedFn (...args) {
// 判斷實參和形參的個數
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments))) // 這里面就是把每次的參數結合起來,再次調用curriedFn
}
}
// 實參和形參個數相同,調用 func,返回結果
return func(...args)
}
}
柯里化總結
- 柯里化可以讓我們給一個函數傳遞較少的參數得到一個已經記住了某些固定參數的新函數
- 這是一種對函數參數的'緩存'
- 讓函數變的更靈活,讓函數的粒度更小
- 可以把多元函數轉換成一元函數,可以組合使用函數產生強大的功能
下篇再整理下函數的組合,以避免柯里化后洋蔥圈似的代碼
函數式編程(三)