函數(shù)式編程中的函數(shù)指的不是程序中的函數(shù)方法乍丈,而是數(shù)學(xué)中的函數(shù)即映射關(guān)系剂碴,是對(duì)運(yùn)算過(guò)程的抽象,是用來(lái)描述數(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ù)式編程語(yǔ)言的特性
函數(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ǔ)下來(lái)并返回
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ù)是用來(lái)抽象通用問(wèn)題忆矛,抽象可以幫我們屏蔽細(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)(語(yǔ)法環(huán)境)的引用捆綁在一起形成閉包嫂侍。
可以在另一個(gè)作用域中調(diào)用一個(gè)函數(shù)的內(nèi)部函數(shù)并訪問(wèn)到該函數(shù)的作用域中的成員
本質(zhì):函數(shù)在執(zhí)行的時(shí)候會(huì)放到一個(gè)執(zhí)行棧上儿捧,當(dāng)函數(shù)執(zhí)行完畢會(huì)從執(zhí)行棧上移除,但是堆上的作用于成員因?yàn)楸煌獠恳貌荒茚尫盘舫瑁虼藘?nèi)部函數(shù)依然可以訪問(wèn)外部函數(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ì)得到相同的輸出,而且沒(méi)有任何可觀察的副作用
純函數(shù)就類似數(shù)學(xué)中的函數(shù)各淀,用來(lái)描述輸入和輸出的關(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é)果緩存起來(lái)
2、可測(cè)試雳旅,純函數(shù)讓測(cè)試更方便
3跟磨、并行處理,在多線程環(huán)境下并行操作共享的內(nèi)存數(shù)據(jù)很有可能出現(xiàn)意外的情況攒盈;純函數(shù)只依賴參數(shù)抵拘,不需要訪問(wèn)共享的內(nèi)存數(shù)據(jù),所以在并行環(huán)境下可以任意運(yùn)行純函數(shù)(web worker)
函數(shù)的副作用:
// 不純的
let mini = 18
function checkAge(age) {
return age >= mini
}
// 純的(有硬編碼型豁,后續(xù)可以通過(guò)柯里化解決)
function checkAge(age) {
let mini = 18
return age >= mini
}
副作用讓一個(gè)純函數(shù)變的不純僵蛛,如上例,純函數(shù)根據(jù)相同的輸入返回相同的輸出偷遗,如果函數(shù)依賴于外部的狀態(tài)就無(wú)法保證輸出相同墩瞳,就會(huì)帶來(lái)副作用
副作用的來(lái)源:配置文件驼壶、數(shù)據(jù)庫(kù)氏豌、獲取用戶的輸入...
所有的外部交互都有可能帶來(lái)副作用,副作用也使得方法通用性下降不適合擴(kuò)展和重用性热凹,同時(shí)副作用會(huì)給程序帶來(lái)安全隱患給程序帶來(lái)不確定性泵喘,但是副作用不可能完全禁止泪电,盡可能控制它們?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)過(guò)多個(gè)函數(shù)處理才能得到最終只突诬,這個(gè)時(shí)候可以把中間過(guò)程的函數(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ù)處理的過(guò)程定義成與數(shù)據(jù)無(wú)關(guān)的合成運(yùn)算旺隙,不需要用到代表數(shù)據(jù)的那個(gè)參
數(shù),只要把簡(jiǎn)單的運(yùn)算步驟合成到一起骏令,在使用這種模式之前我們需要定義一些輔助的基本運(yùn)算函數(shù)蔬捷。
1、不需要指明處理的數(shù)據(jù)
2榔袋、只需要合并運(yùn)算過(guò)程
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)換成大寫(xiě),使用. 作為分隔符
// 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ì)象來(lái)實(shí)現(xiàn)生真,這個(gè)對(duì)象中包含了值,這個(gè)值永遠(yuǎn)不會(huì)對(duì)外公布捺宗,有一個(gè)map方法柱蟀,用來(lái)操作這個(gè)值。還有一個(gè)of方法蚜厉,用來(lái)生成一個(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ì)直接操作值,而是通過(guò)函子來(lái)完成
- 由map處理后返回的是一個(gè)新的對(duì)象,我們可以繼續(xù)鏈?zhǔn)降牟僮髦?/li>
- 我們可以把函子想象成一個(gè)盒子术瓮,盒子中封裝著一個(gè)值康聂,當(dāng)我恩處理盒子中的值的時(shí)候我們要用到盒子專門(mén)改變值的工具:map,我們需要給盒子的map方法傳遞一個(gè)處理值的函數(shù)(純函數(shù))胞四,由這個(gè)函數(shù)來(lái)對(duì)值進(jìn)行處理恬汁,最終map方法返回一個(gè)包含新值的盒子(函子)。
MayBe函子
函子會(huì)接收各種函數(shù)來(lái)處理內(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囚聚,使用左值來(lái)表示錯(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ì)造成函子多層嵌套的問(wèn)題逊脯。取值時(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ù)式編程無(wú)法處理,所以使用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ù)作為值來(lái)處理
- IO函子可以把不純的動(dòng)作存儲(chǔ)到_value中锄贼,延遲這個(gè)不純的操作(惰性執(zhí)行),包裝當(dāng)前的操作是純的操作
- 把不純的操作交給調(diào)用者來(lái)處理