作者:酸菜牛肉 文章內(nèi)容輸出來源:拉勾教育大前端高薪訓(xùn)練營課程
函數(shù)式編程概念:
函數(shù)式編程是一種編程范式沐旨;還有面向?qū)ο缶幊唐椋兔嫦蜻^程編程
- 面向?qū)ο缶幊痰乃季S方式:把現(xiàn)實世界中的事物抽象成程序世界中的類和對象,通過封裝枫疆、繼承和多態(tài)來演示事物事件的聯(lián)系
- 面向?qū)ο缶幊痰乃季S方式:把現(xiàn)實世界的事物和事物之間的聯(lián)系抽象到程序世界(對運算過程進行抽象)
- 程序的本質(zhì): 根據(jù)輸入通過某種運算獲得相應(yīng)輸出吗讶,程序開發(fā)過程中會涉及很多有輸入和輸出的函數(shù)
- x -> f(聯(lián)系躺率、映射)-> y, y=f(x)
- 函數(shù)式編程中的函數(shù)指的不是程序中的函數(shù)(方法),而是數(shù)學(xué)中的函數(shù)即映射關(guān)系棵逊,例如:y = sin(x), x和y的關(guān)系
- 相同的輸入始終要得到相同的輸出(純函數(shù))
- 函數(shù)式編程用來描述數(shù)據(jù)(函數(shù))之間的映射
前置知識:
- 函數(shù)是一等公民
- 高階函數(shù)
- 閉包
函數(shù)是一等公民
- 函數(shù)可以存儲在變量中
- 函數(shù)作為參數(shù)
- 函數(shù)作為返回值
//把函數(shù)賦值給變量
let fn = function() {
console.log('hello world')
}
fn()
const BlogController = {
index(posts){return Views.index(posts) } // 函數(shù)的調(diào)用相同伤疙,可以將后邊函數(shù)本身賦值給變量,(而不是函數(shù)的調(diào)用)
}
// 優(yōu)化
const BlogController = {
index: Views.index
}
高階函數(shù)
- 可以把函數(shù)作為參數(shù)傳遞給兩一個函數(shù)
- 可以把函數(shù)作為另一個函數(shù)的返回結(jié)果
//函數(shù)作為參數(shù)
const filter = (array, fn) => {
let results = []
for(let i =0; i< array.length; i++){
if(fn(array[i]){
results.push(array[i])
}
}
return results
}
let arr = [1, 3, 4, 7, 8]
const a = filter(arr, (item)=>{
return item % 2 === 0
})
//函數(shù)作為函數(shù)返回值
const makeFn = () => {
let msg = 'hello function'
return function(){
console.log(msg);
}
}
makeFn()()
//once
const once = (fn) => {
let done = false
return function(){
if(!done){
done = true
return fn.apply(this, arguments)
}
}
}
let pay = once(function(money)=>{
consle.log(`支付:${money}RMB`)
})
pay(5)
pay(5)
pay(5)
pay(5)
使用高階函數(shù)的意義
- 抽象可以幫我們屏蔽細(xì)節(jié), 只需要關(guān)注與我們的目標(biāo)
- 高階函數(shù)是用來抽象通用的問題
常用高階函數(shù)
- forEach
- map
- filter
- every
- some
- find/findIndex
- reduce
- sort
- ...
const map = (array, fn) => {
let result = []
for (let value of array) {
result.push(fn(value))
}
return result
}
const every = (array, fn) => {
let result = true
for (let value of array) {
result = fn(value)
if(!result){
break
}
}
return result
}
const some = (array, fn) => {
let result = false
for (let value of array) {
result = fn(value)
if(result){
break
}
}
return result
}
let arr = [1, 2, 3, 4]
// arr = map(arr, v => v * v)
// a = every(arr, v => v > 0)
b = some(arr, v => v > 3)
console.log(b)
閉包
閉包: 函數(shù)和其周圍的狀態(tài)(詞法環(huán)境)的引用捆綁在一起形成閉包
可以在另一個作用域中調(diào)用一個函數(shù)的內(nèi)部函數(shù)并訪問到該函數(shù)的作用域中的成員
//閉包徒像;
//once
function once (fn) {
let done = false //此變量的作用范圍被延長黍特,用來標(biāo)記此函數(shù)是否被執(zhí)行
return function () {
if(!done){
done = true
return fn.apply(this, arguments)
}
}
}
let pay = once(function(money){
console.log(`支付: ${money} RMB`)
})
pay(5)
// pay(5)
// pay(5)
閉包的本質(zhì):函數(shù)在執(zhí)行的時候會放在一個執(zhí)行棧上,當(dāng)函數(shù)執(zhí)行完畢之后會從執(zhí)行棧上移除锯蛀,但是堆上的作用域成員因為被外部引用不能被釋放灭衷,因此內(nèi)部函數(shù)依然可以訪問外部函數(shù)的成員。
純函數(shù)
純函數(shù)的定義是:
- 如果函數(shù)的調(diào)用參數(shù)相同旁涤,則永遠(yuǎn)返回相同的結(jié)果翔曲,而且沒有可觀察的副作用
- lodash是一個純函數(shù)的功能庫,提供了對數(shù)組劈愚、數(shù)字瞳遍、對象、字符串菌羽、函數(shù)等操作的一些方法
- 數(shù)組中的slice和splice分別為: 純函數(shù)和不純函數(shù)掠械; slice 返回數(shù)組指定部分,不會改變原數(shù)組注祖; splice 對數(shù)組進行操作返回該數(shù)組猾蒂,會改變原數(shù)組
- 函數(shù)式編程不會保留計算中間的結(jié)果,所以變量是不可變的(無狀態(tài)的)
- 我們可以把一個函數(shù)的執(zhí)行結(jié)果交給兩一個函數(shù)去處理
lodash
純函數(shù)的好處
- 可緩存: 因為純函數(shù)對相同的輸入始終有相同的結(jié)果氓轰,所以可以把純函數(shù)的結(jié)果緩存起來 (memoize)
// 記憶函數(shù)
const _ = require('lodash')
function getArea (r) {
console.log(r)
return Math.PI * r * r
}
// let getAreaWithMemory = _.memoize(getArea)
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
// 模擬 memoize 方法的實現(xiàn)
function memoize (f) {
let cache = {}
return function () {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || f.apply(f, arguments)
return cache[key]
}
}
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
- 可測試: 純函數(shù)讓測試更方便
- 并行處理: 在多線程環(huán)境下操作系統(tǒng)共享的內(nèi)存數(shù)據(jù)很可能會出現(xiàn)意外情況
- 純函數(shù)不需要訪問共享內(nèi)存數(shù)據(jù)婚夫,所以在并行環(huán)境下可以任意運行純函數(shù)(web Worker)
純函數(shù)副作用
如果函數(shù)依賴于外部的狀態(tài)就無法保證輸出相同,就會帶來副作用署鸡。
副作用來源:
- 配置文件
- 數(shù)據(jù)庫
- 獲取用戶的輸入
- ......
所有的外部交互都有可能代理副作用案糙,副作用也使得方法通用性下降不適合擴展和可重用性,同時副作用會給程序中帶來安全隱患靴庆,給程序帶來不確定性时捌,但是副作用不可能完全禁止,盡可能控制他們在可控范圍內(nèi)發(fā)生炉抒。
柯里化 <Currying>
// 函數(shù)的柯里化
// function checkAge (min) {
// return function (age) {
// return age >= min
// }
// }
// ES6
let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
console.log(checkAge18(20))
console.log(checkAge18(24))
- 當(dāng)一個函數(shù)有多個參數(shù)的時候先傳遞一部分參數(shù)調(diào)用它(這部分參數(shù)以后永遠(yuǎn)不變)
- 然后返回一個新的函數(shù)接收剩余的參數(shù)奢讨,返回結(jié)果
lodash 中柯里化
_.curry(func)
- 功能:創(chuàng)建一個函數(shù),該函數(shù)接收一個或多個func的參數(shù)焰薄,如果func所需要的參數(shù)都被提供則執(zhí)行func并返回 執(zhí)行的結(jié)果, 否則繼續(xù)返回該函數(shù)并等待接收剰余的參數(shù).
- 參數(shù):需要柯里化的函數(shù)
- 返回值:柯里化后的函數(shù)
const _ = require('lodash')
const getSum = (a, b, c) => a + b + c
const curried = _.curry(getSum)
console.log(curried(1, 2, 4))
console.log(curried(1, 4)(4))
console.log(curried(14)(4)(3))
// 模擬實現(xiàn) lodash 中的 curry 方法
function getSum (a, b, c) {
return a + b + c
}
const curried = curry(getSum)
console.log(curried(1, 2, 3))
console.log(curried(1)(2, 3))
console.log(curried(1, 2)(3))
function curry (func) {
return function curriedFn(...args) {
// 判斷實參和形參的個數(shù)
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
柯里化總結(jié)
- 柯里化可以數(shù)傳遞較少的參數(shù)得到一個已經(jīng)記住了某些固定參數(shù)
- 這是一種對函數(shù)參數(shù)的“緩存”
- 讓函數(shù)變的更靈活拿诸,讓函數(shù)的粒度更小
- 可以把多元函數(shù)轉(zhuǎn)換成一元函數(shù),可以組合使用函數(shù)產(chǎn)生強大的功能
函數(shù)組合
- 函數(shù)組合(compose)如果一個函數(shù)要經(jīng)過多個函數(shù)處理才能得到最終值(這個時候可挪中間過程的函數(shù)合并成一 個函數(shù))
- 函數(shù)就像是數(shù)據(jù)的管道塞茅,函數(shù)組合就是把這些営道連接起來亩码,讓數(shù)據(jù)穿過多個管道形成最終結(jié)果
- 函數(shù)組合默認(rèn)從右致左執(zhí)行
// 函數(shù)組合演示
function compose (f, g) {
return function (value) {
return f(g(value))
}
}
function reverse (array) {
return array.reverse()
}
function first (array) {
return array[0]
}
const last = compose(first, reverse)
console.log(last([1, 2, 3, 4]))
lodash中的組合函數(shù)
-
lodash
中組合函數(shù)flow()
或者flowRight()
,他們都可以組合多個函數(shù) -
flow()
是從左到右運行 -
flowRight()
是從右到左運行,使用的更多一些
// lodash 中的函數(shù)組合的方法 _.flowRight()
const _ = require('lodash')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const f = _.flowRight(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))
// 模擬 lodash 中的 flowRight
// function compose (...args) {
// return function (value) {
// return args.reverse().reduce(function (acc, fn) {
// return fn(acc)
// }, value)
// }
// }
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é)合律
const _ = require('lodash')
// const f = _.flowRight(_.toUpper, _.first, _.reverse)
// const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
console.log(f(['one', 'two', 'three']))
調(diào)試:
// 函數(shù)組合 調(diào)試
// NEVER SAY DIE --> never-say-die
const _ = require('lodash')
// const log = v => {
// console.log(v)
// return v
// }
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
// _.split()
const split = _.curry((sep, str) => _.split(str, sep))
// _.toLower()
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/fp
- lodash的fp模塊提供了實用的對函數(shù)式編程友好的方法
- 提供了不可變的auto-cuuried iteratee-first data-last 的方法
// lodash 的 fp 模塊
// NEVER SAY DIE --> never-say-die
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))
// 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']))
Point Free
Point Free:我們可以把數(shù)據(jù)處理的過程定義成與數(shù)據(jù)無關(guān)的合成運算野瘦,不需要用到代表數(shù)據(jù)的那個參數(shù)描沟,只要把簡單的運算步驟合成到一起飒泻,在使用這種模式之前我們需要定義一些輔助的基本運算函數(shù).
- 不需要指明處理的數(shù)據(jù)
- 只需要合成運算過程
- 需要定義一些輔助的基本運算函數(shù)
// point free
// Hello World => hello_world
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World'))
// 把一個字符串中的首字母提取并轉(zhuǎn)換成大寫, 使用. 作為分隔符
// world wild web ==> W. W. W
const fp = require('lodash/fp')
// const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
console.log(firstLetterToUpper('world wild web'))
Functor 函子
到目前為止已經(jīng)已經(jīng)學(xué)習(xí)了函數(shù)式編程的一些基礎(chǔ),但是我們還沒有演示在函數(shù)式編程中如何把副作用控制在可控范圍內(nèi)吏廉、異常處理泞遗、異步操作等.
什么是函子:
- 容器:包含值和值的變形關(guān)系(這個變形關(guān)系就是函數(shù))
- 函 子 : 是一個特殊的容器,通過一個普通的對象來實現(xiàn)席覆,該對象具有map方法史辙,map方法可以運行一個函數(shù)對值進行處理(變形關(guān)系)
// // Functor 函子
// class Container {
// constructor (value) {
// this._value = value
// }
// map (fn) {
// return new Container(fn(this._value))
// }
// }
// let r = new Container(5)
// .map(x => x + 1)
// .map(x => x * x)
// console.log(r)
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 + 2)
// .map(x => x * x)
// console.log(r)
// 演示 null undefined 的問題
Container.of(null)
.map(x => x.toUpperCase())
總結(jié):
- 函數(shù)式編程的運算不直接操作值,而是由函子完成
- 函子就是一個實現(xiàn)了map契約的對象
- 我們可以把函子想象成一個盒子娜睛,這個盒子里封裝了一值
- 想要處理盒子的值髓霞,我們需要給盒子的map方法傳遞一個處理值的函數(shù)(純函數(shù)),由這個函數(shù)來對值進行處理
- 最終map方法返回包含一個新值的盒子(函子)
MayBe 函子
- 可以對外部的空值情況做處理(控制副作用在允許的范圍)
// MayBe 函子
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
isNothing () {
return this._value === null || this._value === undefined
}
}
// let r = MayBe.of('Hello World')
// .map(x => x.toUpperCase())
// console.log(r)
// let r = MayBe.of(null)
// .map(x => x.toUpperCase())
// console.log(r)
let r = MayBe.of('hello world')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
console.log(r)
Either 函子
Either 函子
- Either 兩者中的任何一個畦戒,類似if...else...的處理
- 異常會讓函數(shù)變得不純方库, Either函子可以用來做異常處理
// Either 函子
class Left {
static of (value) {
return new Left(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this
}
}
class Right {
static of (value) {
return new Right(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return Right.of(fn(this._value))
}
}
// let r1 = Right.of(12).map(x => x + 2)
// let r2 = Left.of(12).map(x => x + 2)
// console.log(r1)
// console.log(r2)
function parseJSON (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({ error: e.message })
}
}
// let r = parseJSON('{ name: zs }')
// console.log(r)
let r = parseJSON('{ "name": "zs" }')
.map(x => x.name.toUpperCase())
console.log(r)
IO函子
- IO函子中的_value 是一個函數(shù),這里吧函數(shù)作為值來處理
- IO函子可以把不純的動作儲存到_value中障斋,延遲執(zhí)行這個不純的操作(惰性執(zhí)行)纵潦,包裝當(dāng)前的操作純
- 把不純的操作交給調(diào)用者來處理
// 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))
}
}
// 調(diào)用
let r = IO.of(process).map(p => p.execPath)
// console.log(r)
console.log(r._value())
Task異步執(zhí)行
- 異步任務(wù)的實現(xiàn)過于復(fù)雜,我們使用folktale中的Task來演示
- folktale—個標(biāo)準(zhǔn)的函數(shù)式編程庫: 和lodash垃环、ramda不同的是邀层,他沒有提供很多功能函數(shù); 只提供了一些函數(shù)式處理的操作,例如:compose遂庄、curry等寥院,一些函子Task、Either涛目、MayBe等
// Task 處理異步任務(wù)
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
function readFile (filename) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value)
}
})
// IO 函子的問題
const fs = require('fs')
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))
}
}
let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
let print = function (x) {
return new IO(function () {
console.log(x)
return x
})
}
let cat = fp.flowRight(print, readFile)
// IO(IO(x))
let r = cat('package.json')._value()._value()
console.log(r)
monad 函子
解決函子嵌套問題
// IO Monad
const fs = require('fs')
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))
}
join () {
return this._value()
}
flatMap (fn) {
return this.map(fn).join()
}
}
let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
let print = function (x) {
return new IO(function () {
console.log(x)
return x
})
}
let r = readFile('package.json')
// .map(x => x.toUpperCase())
.map(fp.toUpper)
.flatMap(print)
.join()
console.log(r)
總結(jié):
文章內(nèi)容輸出來源于:拉勾教育大前端高薪訓(xùn)練營