函數(shù)式編程是一種編程范式,和面向?qū)ο缶幊坛什⒘嘘P(guān)系涩堤。
- 面向?qū)ο缶幊蹋簩ΜF(xiàn)實(shí)世界中事物的抽象,抽象出對象以及對象和對象之間的關(guān)系;
- 函數(shù)式編程:把現(xiàn)實(shí)世界事物和事物之間的聯(lián)系抽象到程序世界(對運(yùn)算過程進(jìn)行抽象)娃循。
推薦書籍
- 你不知道Javascript
- Javascript忍者秘籍
- Javascript20years
函數(shù)是一等公民
- 函數(shù)可以存儲(chǔ)在變量中;
- 函數(shù)作為參數(shù)斗蒋;
- 函數(shù)作為返回值捌斧。
原理:在js中函數(shù)的數(shù)據(jù)類型為對象。
高階函數(shù)
可以把函數(shù)作為參數(shù)/返回值泉沾。
意義:屏蔽細(xì)節(jié)捞蚂,便于抽象。
常用的高階函數(shù):
- forEach
- map
- filter
- every:檢測數(shù)組中的所有元素是否符合條件跷究,有一個(gè)不符合返回false
- some:檢測數(shù)組中是否有函數(shù)符合條件姓迅,有一個(gè)符合返回true
- find/findIndex:返回?cái)?shù)組中滿足條件的第一個(gè)元素的值/索引,沒有則返回undefined、-1
- reduce:接收一個(gè)函數(shù)作為累加器丁存,數(shù)組中的值從左到右肩杈,上一個(gè)輸出作為下一次迭代的輸入,最后返回一個(gè)值解寝±┤唬可以作為一個(gè)高階函數(shù)用于函數(shù)組合
- sort
this指向
改變函數(shù)this指向?qū)Ψ椒ǎ篵ind(不調(diào)用)、call编丘、apply
- 模擬bind實(shí)現(xiàn)
// 模擬bind實(shí)現(xiàn)
Function.prototype.myBind = function (context, ...args) {
return (...rest) => this.call(context, ...args, ...rest)
}
執(zhí)行上下文
全局執(zhí)行上下文
-
函數(shù)級執(zhí)行上下文
函數(shù)的執(zhí)行階段可以分為:
1.函數(shù)建立階段:當(dāng)調(diào)用函數(shù)時(shí)与学,還沒有執(zhí)行函數(shù)內(nèi)部的代碼- variableObject(VO):收集函數(shù)中的arguments、參數(shù)嘉抓、內(nèi)部成員索守;
- scopeChains:詞法環(huán)境,作用域鏈抑片,記錄當(dāng)前函數(shù)所在父級作用域中的活動(dòng)對象卵佛;[[Scopes]]作用域鏈,函數(shù)在創(chuàng)建時(shí)就會(huì)生成該屬性敞斋,js引擎才可以訪問截汪,這個(gè)屬性中存儲(chǔ)的是所有父級中的變量對象。
- this:當(dāng)前函數(shù)內(nèi)部的this指向植捎,this的指向是動(dòng)態(tài)確定的衙解,當(dāng)函數(shù)在調(diào)用時(shí)才能確定。
2.函數(shù)執(zhí)行階段
- activationObject(AO):用AO指向VO
eval執(zhí)行上下文
閉包
能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)焰枢。
本質(zhì):函數(shù)在執(zhí)行時(shí)會(huì)被放入執(zhí)行棧蚓峦,當(dāng)函數(shù)執(zhí)行完畢會(huì)從執(zhí)行棧上移除,但堆上的作用域成員因?yàn)?strong>被外部引用不能釋放济锄,因此內(nèi)部函數(shù)依然可以訪問外部函數(shù)的成員暑椰。
作用:
1.可以讀取函數(shù)內(nèi)部的變量;
2.讓這些變量的值始終保持在內(nèi)存中荐绝。
使用注意點(diǎn):
1.由于閉包會(huì)使函數(shù)中的變量都被保存在內(nèi)存中一汽,因此濫用閉包會(huì)造成性能問題。解決方法:在退出函數(shù)之前低滩,刪除所有無用的變量召夹。
2.不要隨意修改父函數(shù)中變量的值。
// once
function once(fn) {
let done = false
return function () {
if (!done) {
done = true
// arguments:類數(shù)組對象恕沫,存儲(chǔ)傳入函數(shù)的所有參數(shù)戳鹅。具有數(shù)組的部分屬性,比如.length昏兆、索引
// apply(obj, args):
// 劫持另一個(gè)對象的方法枫虏,繼承另一個(gè)對象的屬性妇穴,obj代替function中的this對象,args作為參數(shù)傳遞給函數(shù)args-->arguments
return fn.apply(this, arguments)
}
}
}
純函數(shù)(??重點(diǎn)掌握)
相同的輸入永遠(yuǎn)會(huì)得到相同的輸出隶债,類似于數(shù)學(xué)中的函數(shù)關(guān)系腾它,沒有任何可觀察的副作用(副作用不可能完全禁止)。
純函數(shù)的好處
- 可緩存
// 模擬memorize
function memorize(fn) {
let cache = {}
return function () {
let arg_str = JSON.stringify(arguments)
cache[arg_str] = cache[arg_str] || fn.apply(fn, arguments)
return cache[arg_str]
}
}
- 可測試
- 并行處理
柯里化(??重點(diǎn)掌握)
當(dāng)一個(gè)函數(shù)有多個(gè)參數(shù)時(shí)先傳遞一部分參數(shù)調(diào)用它(這部分參數(shù)以后永遠(yuǎn)不變)死讹,然后返回一個(gè)參數(shù)接收剩余的參數(shù)瞒滴,返回結(jié)果。
對函數(shù)進(jìn)行降維赞警,為函數(shù)組合作準(zhǔn)備妓忍。
lodash中的柯里化函數(shù)
// 模擬_.curry()的實(shí)現(xiàn)
function curry(fn) {
// ...args-剩余參數(shù):將不定量的參數(shù)表示為一個(gè)數(shù)組
// 剩余參數(shù)和arguments的區(qū)別:
// 1.剩余參數(shù)只包含那些沒有對應(yīng)形參的實(shí)參,而arguments包含傳給函數(shù)的所有實(shí)參愧旦;
// 2.arguments對象不是一個(gè)真正的數(shù)組世剖,而剩余參數(shù)是一個(gè)真正的Array實(shí)例;
// 3.arguments還有一些剩余的屬性笤虫。
return function curriedFn(...args) {
// 判斷實(shí)參和形參的個(gè)數(shù)
// function.length -> 函數(shù)形參的個(gè)數(shù)
if (args.length < fn.length) {
return curriedFn(...args.concat(Array.from(arguments)))
}
return fn(...args) // args展開
}
}
函數(shù)組合(??重點(diǎn)掌握)
如果一個(gè)函數(shù)要經(jīng)過多個(gè)函數(shù)處理才能得到最終值旁瘫,可以把中間過程的函數(shù)組合為一個(gè)函數(shù)。順序默認(rèn)從右向左執(zhí)行琼蚯。
// 模擬lodash中的flowRight
function compose(...fns) {
return function (value) {
return fns.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value) // value為初始值
}
}
// 使用鉤子函數(shù)
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)
如何調(diào)試函數(shù)組合
可以編寫一個(gè)接收“當(dāng)前位置標(biāo)記”和value并返回value的柯里化函數(shù)酬凳,插入到函數(shù)組合中,用于追蹤哪一步出錯(cuò)遭庶。
// 'NEVER SAY DIE' --> 'never-say-die'
// 調(diào)試
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, arr) => _.join(arr, sep))
const map = _.curry((fn, arr) => _.map(arr, fn))
// 錯(cuò)誤
const f = _.flowRight(join('-'), trace('map之后'), _.toLower, trace('map之前'), split(' '))
// 正確
const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower), trace('map之前'), split(' '))
console.log('NEVER SAY DIE');
lodash/fp
提供了對函數(shù)式編程友好的方法宁仔,提供了不可變自動(dòng)柯里化、函數(shù)優(yōu)先峦睡、數(shù)據(jù)滯后的方法翎苫。
Functor(函子)
函子就是一個(gè)裝有一個(gè)變量的容器,通過map方法維護(hù)這個(gè)變量赐俗。
函子在開發(fā)中的實(shí)際應(yīng)用場景:作用是空值副作用(IO)、異常處理(Either)弊知、異步任務(wù)(Task)阻逮。
// Functor 函子
// 函數(shù)式的編程不直接操作值,而是由函子完成
class Container{
// of靜態(tài)方法秩彤,可以省略new關(guān)鍵字創(chuàng)建對象
static of(value) {
return new Container(value)
}
constructor(value) {
this._value = value
}
// map方法叔扼,傳入處理value的函數(shù),返回一個(gè)包含新值的函子
map(fn) {
return Container.of(fn(this._value))
}
}
MayBe函子
// MayBe 函子
// 處理空置異常
class MayBe {
static of(value) {
return new MayBe(value)
}
constructor(value) {
this._value = value
}
map(fn) {
return MayBe.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
isNothing() {
return this._value === null || this._value === undefined
}
}
Either函子
// Either函子
// 傳入空值調(diào)用的函子
class Left {
static of(value) {
return new Left(value)
}
constructor(value) {
this._value = value
}
map(fn) {
return this
}
}
// 正常情況下調(diào)用的函子
class Right {
static of(value) {
return new Right(value)
}
constructor(value) {
this._value = value
}
map(fn) {
return Right.of(fn(this._value))
}
}
// Either用來處理異常
function parseJSON(json) {
try {
return Right.of(JSON.parse(json))
} catch (e) {
return Left.of({ error: e.message })
}
}
IO函子
IO函子中的_value是一個(gè)函數(shù)漫雷,可以把不純的動(dòng)作存儲(chǔ)到_value中瓜富,延遲這個(gè)不純的操作。
const fp = require('lodash/fp')
// IO函子
class IO {
static of(value) {
// 給當(dāng)前傳入的value包裹一層函數(shù)降盹,讓不純的操作滯后發(fā)生与柑,保證當(dāng)前函數(shù)相同輸入得到相同輸出。
// 將不純的操作交給調(diào)用者處理。
return new IO(function () {
return value
})
}
constructor(fn) {
this._value = fn
}
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
Task異步執(zhí)行
// folktale中的task函子价捧,處理異步執(zhí)行
function readFile(filename) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
// 調(diào)用run執(zhí)行
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value)
}
})
Pointed函子
是實(shí)現(xiàn)了of靜態(tài)方法的函子丑念,更深層的意義是將值放到上下文Context(把值放到容器中,使用map處理值)结蟋。
Monad(單子)
是可以變扁的Pointed函子脯倚,具有join和of兩個(gè)方法,能夠解決IO函子中出現(xiàn)的多層嵌套問題嵌屎。
const fp = require('lodash/fp')
// IO Monad
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)用后的值推正,減少一層嵌套
join() {
return this._value()
}
// 拍平map
flatMap(fn) {
return this.map(fn).join()
}
}