函數(shù)式編程中的函數(shù)指的不是程序中的函數(shù)方法,而是數(shù)學(xué)中的函數(shù)即映射關(guān)系,是對(duì)運(yùn)算過程的抽象,是用來描述數(shù)據(jù)之間的映射
// 非函數(shù)式
let a = 1, b = 2, c = a + b;
console.log(c)
// 函數(shù)式
const aa = (a, b) => {
return a + b
}
let c = aa(1, 2)
console.log(c)
函數(shù)式編程語言的特性
函數(shù)是一等公民
- 函數(shù)可以存儲(chǔ)在變量中
- 函數(shù)可以作為參數(shù)
- 函數(shù)可以作為返回值
高階函數(shù)
什么是高階函數(shù)?
- 函數(shù)可以作為參數(shù)傳遞給另一個(gè)函數(shù)
- 函數(shù)可以作為另一個(gè)函數(shù)的返回結(jié)果
函數(shù)作為參數(shù)
// 模擬forEach,打印數(shù)組中的每一項(xiàng)
const forEach = (arr, fn) => {
for (let i = 0; i < arr.length; i++) {
fn(arr[i])
}
}
let c = [1, 2, 3, 4, 5];
forEach(arr, (item) => {
console.log(item)
})
// 模擬filter,把滿足條件的每一項(xiàng)存儲(chǔ)下來并返回
const filter = (arr, fn) => {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (fn(arr[i])) {
result.push(arr[i])
}
}
return result
}
// 測(cè)試
let arr = [1, 2, 4, 7, 8];
filter(arr, (item) => {
return item % 2 === 0
})
函數(shù)作為返回值
const makeFn = () => {
let msg = 'hello function';
return () => {
console.log(msg)
}
}
makeFn()();
// 模擬lodash中的once,只執(zhí)行一次
const once = (fn) => {
let done = false;
return function () {
if (!done) {
done = true;
return fn.apply(this, arguments);
}
}
}
let pay = once((money) => {
console.log('gg', money)
console.log(`支付了${money}RMB`)
});
pay(9);
使用高階函數(shù)的意義
高階函數(shù)是用來抽象通用問題,抽象可以幫我們屏蔽細(xì)節(jié),我們只用關(guān)注實(shí)現(xiàn)的目標(biāo)
// 模擬常用的高階函數(shù):map、every、some
// map 遍歷數(shù)組中的每一項(xiàng),將滿足條件的項(xiàng)存入新的數(shù)組并返回
const map = (array, fn) => {
let result = [];
for (let value of array) {
result.push(fn(value))
}
return result
}
// 測(cè)試
console.log(map([1, 2, 3, 4], v => v * v))
// every 檢測(cè)數(shù)組所有元素是否都符合指定條件,有一項(xiàng)不滿足條件就返回false,剩余的元素不會(huì)再進(jìn)行檢測(cè)。
const every = (array, fn) => {
let result = true;
for (let value of array) {
if (!fn(value)) {
result = false;
break;
}
}
return result
}
// 測(cè)試
console.log(every([5, 7, 6,], v => v > 10));
// some 檢測(cè)數(shù)組中的元素是否滿足指定條件,如果有一個(gè)元素滿足指定條件就返回true,剩余的元素不會(huì)再繼續(xù)檢測(cè)。
const some = (array, fn) => {
let result = true;
for (let value of array) {
if (fn(value)) {
result = true;
}
}
return result
}
// 測(cè)試
console.log(some([5, 8, 9, 3], (v) => v > 2));
閉包
含義:
函數(shù)和其周圍的狀態(tài)(語法環(huán)境)的引用捆綁在一起形成閉包。
可以在另一個(gè)作用域中調(diào)用一個(gè)函數(shù)的內(nèi)部函數(shù)并訪問到該函數(shù)的作用域中的成員
本質(zhì):函數(shù)在執(zhí)行的時(shí)候會(huì)放到一個(gè)執(zhí)行棧上,當(dāng)函數(shù)執(zhí)行完畢會(huì)從執(zhí)行棧上移除,但是堆上的作用于成員因?yàn)楸煌獠恳貌荒茚尫?,因此?nèi)部函數(shù)依然可以訪問外部函數(shù)的成員。
function makePower(power) {
return function (number) {
return Math.pow(number, power)
}
}
// 求平方
let power2 = makePower(2);
// 求立方
let power3 = makePower(3);
console.log(power2(2))
console.log(power2(3))
console.log(power3(4))
純函數(shù)
純函數(shù)的概念
相同的輸入永遠(yuǎn)會(huì)得到相同的輸出,而且沒有任何可觀察的副作用
純函數(shù)就類似數(shù)學(xué)中的函數(shù),用來描述輸入和輸出的關(guān)系
lodash是一個(gè)純函數(shù)的功能庫(kù),提供了對(duì)數(shù)組,數(shù)字,對(duì)象,字符串,函數(shù)等操作方法
-
數(shù)組的slice和splic分別是純函數(shù)和不純的函數(shù)
slice返回?cái)?shù)組中的指定部分,不會(huì)改變?cè)瓟?shù)組
splice對(duì)數(shù)組進(jìn)行操作返回該數(shù)組,會(huì)改變?cè)瓟?shù)組
let numbers = [1, 2, 3, 4, 5]
//純函數(shù)
numbers.slice(0, 3) // => [1, 2, 3]
numbers.slice(0, 3) // => [1, 2, 3]
numbers.slice(0, 3) // => [1, 2, 3]
// 不純的函數(shù)
numbers.splice(0, 3) // => [1, 2, 3]
numbers.splice(0, 3) // => [4, 5]
numbers.splice(0, 3) // => []
純函數(shù)代表:lodash
純函數(shù)的好處:
1、可緩存,因?yàn)榧兒瘜?duì)于相同輸入始終具有相同輸出,所以可以把純函數(shù)的結(jié)果緩存起來
2、可測(cè)試,純函數(shù)讓測(cè)試更方便
3、并行處理,在多線程環(huán)境下并行操作共享的內(nèi)存數(shù)據(jù)很有可能出現(xiàn)意外的情況;純函數(shù)只依賴參數(shù),不需要訪問共享的內(nèi)存數(shù)據(jù),所以在并行環(huán)境下可以任意運(yùn)行純函數(shù)(web worker)
函數(shù)的副作用:
// 不純的
let mini = 18
function checkAge(age) {
return age >= mini
}
// 純的(有硬編碼,后續(xù)可以通過柯里化解決)
function checkAge(age) {
let mini = 18
return age >= mini
}
副作用讓一個(gè)純函數(shù)變的不純,如上例,純函數(shù)根據(jù)相同的輸入返回相同的輸出,如果函數(shù)依賴于外部的狀態(tài)就無法保證輸出相同,就會(huì)帶來副作用
副作用的來源:配置文件、數(shù)據(jù)庫(kù)、獲取用戶的輸入...
所有的外部交互都有可能帶來副作用,副作用也使得方法通用性下降不適合擴(kuò)展和重用性,同時(shí)副作用會(huì)給程序帶來安全隱患給程序帶來不確定性,但是副作用不可能完全禁止,盡可能控制它們?cè)诳煽胤秶鷥?nèi)發(fā)生。
柯里化
當(dāng)一個(gè)函數(shù)有多個(gè)參數(shù)的時(shí)候先傳遞一部分參數(shù)調(diào)用它(這部分參數(shù)以后永遠(yuǎn)不變),然后返回一個(gè)新的函數(shù)接收剩余的參數(shù),返回結(jié)果
lodash中的柯里化
// 柯里化案例
''.match(/\s+/g);// 提取字符串中的空白字符
''.match(/\d+/g);// 提取字符串中的數(shù)字
const _ = require('lodash');
const match = _.curry((reg, str) => str.match(reg));
const haveSpace = match(/\s+/g);
const haveNumber = match(/\d+/g);
// console.log(haveSpace('hello word'))
// console.log(haveNumber('333adg'))
// 操作數(shù)組
const filter = _.curry((func, array) => array.filter(func))
// 獲取數(shù)組中具有空白字符的元素
const findSpace = filter(haveSpace);
console.log(findSpace(['fsf,vvv ,kkk l']));
柯里化實(shí)現(xiàn)原理
// 模擬柯里化實(shí)現(xiàn)原理
const getSum = (a, b, c) => a + b + c;
// 模擬lodash中curry方法
const curry = (func) => {
return function curriedFn(...args) {
// 判斷形參和實(shí)參的個(gè)數(shù)
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
const curried = curry(getSum);
console.log(curried(1)(2, 3)); // 6
console.log(curried(1, 2)(3));// 6
console.log(curried(1, 2, 3));// 6
總結(jié)
柯里化可以讓我們給一個(gè)函數(shù)傳遞較少的參數(shù)得到一個(gè)已經(jīng)記住啦某些固定參數(shù)的新函數(shù)
這是一種對(duì)函數(shù)參數(shù)的緩存
讓函數(shù)變得更靈活,讓函數(shù)的粒度更小
可以把多元函數(shù)轉(zhuǎn)成一元函數(shù),可以組合使用函數(shù)產(chǎn)生強(qiáng)大的功能
函數(shù)組合
如果一個(gè)函數(shù)需要經(jīng)過多個(gè)函數(shù)處理才能得到最終只,這個(gè)時(shí)候可以把中間過程的函數(shù)組合成一個(gè)函數(shù)
// 函數(shù)組合
const compose = (f, g) => {
return function (value) {
return f(g(value))
}
}
// 反轉(zhuǎn)數(shù)組
const reverse = (array) => {
return array.reverse()
}
// 獲取數(shù)組的第一個(gè)元素
const first = (array) => {
return array[0]
}
const last = compose(first, reverse);
console.log(last([1, 2, 3, 4, 5, 6,]));
lodash中的組合函數(shù)
// 模擬lodash中的flowRight
// const _ = require('lodash');
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const toUpper = s => s.toUpperCase();
// const compose = (...args) => {
// return (value) => {
// return args.reverse().reduce((acc, fn) => {
// return fn(acc)
// }, value)
// }
// }
// ES6
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const f = compose(toUpper, first, reverse);
console.log(f(['one', 'two', 'three']));
函數(shù)結(jié)合律
// 函數(shù)結(jié)合需要滿足結(jié)合律
const _ = require('lodash');
const f = _.flowRight(_.toUpper, flowRight(_.first, _.reverse));
console.log(f(['one', 'two', 'three']));
函數(shù)組合調(diào)試
// 函數(shù)結(jié)合 調(diào)試
// NEVER SAY DIE --->never-say-die
const _ = require('lodash');
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, array) => _.join(array, sep))
const map = _.curry((fn, array) => _.map(array, fn));
const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower),trace('split之后'), split(' '));
console.log(f('NEVER SAY DIE'))
lodash模塊數(shù)據(jù)優(yōu)先,函數(shù)置后
lodash/fp模塊函數(shù)優(yōu)先,數(shù)據(jù)置后
// lodash和lodash/fp模塊中map方法的區(qū)別
const _ = require('lodash');
console.log(_map(["23", "8", "10"]), parseInt);
// parseInt("23",0,array)
// parseInt("8",1,array)
// parseInt("10",2,array)
const fp = require('lodash/fp');
console.log(fp.map(parseInt, ["23", "8", "10"]))
PointFree
我們可以把數(shù)據(jù)處理的過程定義成與數(shù)據(jù)無關(guān)的合成運(yùn)算,不需要用到代表數(shù)據(jù)的那個(gè)參
數(shù),只要把簡(jiǎn)單的運(yùn)算步驟合成到一起,在使用這種模式之前我們需要定義一些輔助的基本運(yùn)算函數(shù)。
1、不需要指明處理的數(shù)據(jù)
2、只需要合并運(yùn)算過程
3、需要定義一些輔助的基本運(yùn)算函數(shù)
const f = fp.flowRight(fp.join('-'), trace('map之后'), fp.map(fp.toLower), trace('split之后'), fp.split(' '));
案例
// 非pointFree模式
// const f = (word) => word.toLowerCase().replace(/\s+/g, '_');
// pointFree模式
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, "_"), fp.toLower);
console.log(f('hello word'))
//把一個(gè)字符串的首字符提取并轉(zhuǎn)換成大寫,使用. 作為分隔符
// word wild web ==>W. W. W
// pointFree模式
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(fp.join('. '),fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '))
console.log(firstLetterToUpper('word wild web'))
函子
函子的概念
函子是函數(shù)式編程里面最重要的數(shù)據(jù)類型,也是基本的運(yùn)算單位和功能單位。
函子首先是一個(gè)容器,它包含了值和值的變形關(guān)系,這個(gè)變形關(guān)系就是函數(shù)。
函子可以把函數(shù)式編程副作用控制在可控的范圍內(nèi),包括處理異常,異步操作等。
一般約定,函子的標(biāo)志就是容器具有map方法。該方法將容器里面的每一個(gè)值,映射到另一個(gè)容器。
函子的基本構(gòu)造
函子就是一個(gè)特殊的容器,它可以由對(duì)象來實(shí)現(xiàn),這個(gè)對(duì)象中包含了值,這個(gè)值永遠(yuǎn)不會(huì)對(duì)外公布,有一個(gè)map方法,用來操作這個(gè)值。還有一個(gè)of方法,用來生成一個(gè)新的容器。
// Functor
class Container {
static of(value) {
return new Container(value)
}
constructor(value) {
this._value = value;
}
map(fn) {
return Container.of((fn(this._value)))
}
}
let r = Container.of(5).map(x => x + 1).map(x => x * x);
console.log('r', r);
這里總結(jié)一下函子的使用
- 程序運(yùn)算不會(huì)直接操作值,而是通過函子來完成
- 由map處理后返回的是一個(gè)新的對(duì)象,我們可以繼續(xù)鏈?zhǔn)降牟僮髦?/li>
- 我們可以把函子想象成一個(gè)盒子,盒子中封裝著一個(gè)值,當(dāng)我恩處理盒子中的值的時(shí)候我們要用到盒子專門改變值的工具:map,我們需要給盒子的map方法傳遞一個(gè)處理值的函數(shù)(純函數(shù)),由這個(gè)函數(shù)來對(duì)值進(jìn)行處理,最終map方法返回一個(gè)包含新值的盒子(函子)。
MayBe函子
函子會(huì)接收各種函數(shù)來處理內(nèi)部的值,這里就有可能遇到錯(cuò)誤,我們需要對(duì)這些錯(cuò)誤做處理,MayBe函子的作用就是對(duì)外部的空值情況做處理。
MayBe函子的構(gòu)造就是在map中設(shè)置空值檢查
class Maybe{
static of(value){
return new Maybe(value)
}
constructor(val){
this._value = val
}
map(f) {
return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
}
}
雖然 MayBe函子可以避免出現(xiàn)錯(cuò)誤,但是多次調(diào)用map時(shí)我們并不知道哪里出現(xiàn)了錯(cuò)誤
Either函子
Either函子與if...else處理很相似。它內(nèi)部有兩個(gè)值,左值和右值。右值通常代表正常的值,左值是當(dāng)右值不存在或錯(cuò)誤時(shí)的默認(rèn)值
class Either {
static of(left,right){
return new Either (left,right))
}
constructor(left,right){
this.left = left
this.right = right
}
map(fn){
return this.right ? Either.of(this.left,fn(this.right)) : Either.of(fn(this.left),right)
}
}
此外,Either函子另一個(gè)用途是替代try...catch,使用左值來表示錯(cuò)誤
function parseJSON(json) {
try {
return Either.of(null, JSON.parse(json));
} catch (e: Error) {
return Either.of(e, null);
}
}
ap函子
函子中的值有可能是數(shù)值,也有可能是一個(gè)函數(shù),我們想讓值為函數(shù)的函子用另一個(gè)函子中的值運(yùn)算,我們就可以用ap函子
function add(x) {
return x + 1
}
const A = Functor.of(2)
const B = Functor.of(add)
class Ap extends Functor {
ap(F) {
return Ap.of(this.val(F.val))
}
}
//我們想讓B函子的值使用A函子的值
Ap.of(add).ap(Functor.of(2))
凡是部署了ap方法的函子,就是ap函子。ap函子的意義在于對(duì)多參數(shù)的函數(shù),可以從多個(gè)容器中取值,實(shí)現(xiàn)函子的鏈?zhǔn)秸{(diào)用。
Monad 函子
函子中的值可以接受任何值,所以函子之中可以包含另一個(gè)函子。這樣就會(huì)造成函子多層嵌套的問題。取值時(shí)會(huì)很不方便。Monad函子的作用就是:總是返回一個(gè)單層的函子,它有一個(gè)FlatMap方法,與map方法作用相同,唯一的區(qū)別就是如果生成了一個(gè)嵌套函子,它會(huì)取出后者的值,保證返回的永遠(yuǎn)是一個(gè)單層的容器,不會(huì)出現(xiàn)嵌套的情況。
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {//f是一個(gè)函子
return this.map(f).join();
}
}
如果函數(shù)f返回的是一個(gè)函子,那么this.map(f)就會(huì)生成一個(gè)嵌套的函子。所以,join方法保證了flatMap方法總是返回一個(gè)單層的函子。這意味著嵌套的函子會(huì)被鋪平。
IO函子
I/O是一個(gè)不純的操作,普通的函數(shù)式編程無法處理,所以使用IO函子操作
const fp = require('lodash/fp')
class IO {
static of (value) {
return new IO (function () {
return value
})
}
constructor(fn) {
this._value = fn
}
map (fn){
return new IO (fp.flowRight(fn,this._value));
}
}
- IO函子中的_value是一個(gè)一個(gè)函數(shù),這里是把函數(shù)作為值來處理
- IO函子可以把不純的動(dòng)作存儲(chǔ)到_value中,延遲這個(gè)不純的操作(惰性執(zhí)行),包裝當(dāng)前的操作是純的操作
- 把不純的操作交給調(diào)用者來處理