>>函數(shù)式編程:
原則:小
f(x) = y[數(shù)學(xué)函數(shù)]
1脖镀、函數(shù)必須總是接受一個參數(shù);
2狼电、函數(shù)必須總時返回一個值蜒灰;
3弦蹂、函數(shù)應(yīng)該依據(jù)接收到的參數(shù)(例如x),而不是外部環(huán)境運(yùn)行强窖;
4凸椿、對于一個給定的x,只會輸出唯一一個y翅溺。
函數(shù)式編程是一種范式脑漫,我們能夠以此創(chuàng)建僅依賴輸入就可以完成自身邏輯的函數(shù)。這保證了當(dāng)函數(shù)被多次調(diào)用時任然
返回相同的結(jié)果咙崎。函數(shù)不會改變?nèi)魏瓮獠凯h(huán)境的變量优幸,這將產(chǎn)生可緩存的,可測試的代碼庫褪猛。
引用透明性:所有的函數(shù)對于相同的輸入都將返回相同值
替換模型:直接替換函數(shù)的結(jié)果(主要因?yàn)楹瘮?shù)的邏輯不依賴其他全局的變量)网杆,這使并發(fā)代碼和緩存成為可能。
函數(shù)式編程主張聲明式編程和編寫抽象的代碼握爷。
(區(qū)分聲明式編程和命令式編程:for 和 forEach)
“如何”做的部分將被抽象到普通函數(shù)中(高階函數(shù))
函數(shù)式編程主張以抽象的方式創(chuàng)建函數(shù)跛璧,這些函數(shù)能夠在代碼的其他部分被重用。
抽象:把復(fù)雜的東西抽出來新啼,變成簡單的東西追城。
>>純函數(shù):
純函數(shù)是數(shù)學(xué)函數(shù)
純函數(shù)是對給定的輸入返回相同的輸出的函數(shù)
->產(chǎn)生可測試的代碼(純函數(shù)不應(yīng)改變?nèi)魏瓮獠凯h(huán)境的變量)
->合理的代碼:必須具有一個有意義的名稱(通過函數(shù)名能夠輕易地推理出該函數(shù)的作用)
=>包含純函數(shù)的代碼易于閱讀、理解和測試燥撞。
js不是一種純函數(shù)語言(因?yàn)榭梢圆挥脜?shù)傳入)
js支持函數(shù)作為參數(shù)座柱,以及將函數(shù)傳值給另一函數(shù)等特性(js將函數(shù)視為一等公民)
>>高階函數(shù):
高階函數(shù):接受另一個函數(shù)作為其參數(shù)的函數(shù)稱為高階函數(shù)。
// JS七種基本類型
{
Number
String
Boolean
Object
Null
Undefined
Symbol
}
高階函數(shù)是接受函數(shù)作為參數(shù)并且/或者返回函數(shù)作為輸出的函數(shù)冠胯。
大多數(shù)高階函數(shù)與閉包一起使用火诸。
>>閉包
閉包與作用域相關(guān)聯(lián):
outer(){
inner(){
...
}
}
inner是閉包,inner能訪問自身作用域的變量荠察,也能訪問全局變量置蜀,也能訪問外部函數(shù)父級(outer)作用域的變量。
- 閉包能訪問外部函數(shù)的變量悉盆,該屬性使閉包變量非常強(qiáng)大盯荤!
- 閉包可以記住它的上下文。
>>柯里化函數(shù)(curry):
(高階函數(shù)和閉包構(gòu)成柯里化函數(shù))
把一個多參數(shù)函數(shù)轉(zhuǎn)換為一個嵌套的一元函數(shù)的過程焕盟;
注:curry函數(shù)應(yīng)用參數(shù)列表的順序是從左到最右
- 兩元函數(shù)轉(zhuǎn)柯里化
let curry = (fn) => {
return function (x) {
return function (y) {
return fn(x,y)
}
}
}
let add = function (x,y) {
return x+y
}
curry(add)(2)(3) // => 5
- 多參數(shù)函數(shù)轉(zhuǎn)化一元函數(shù)的curry函數(shù)
let curryN = (fn) => {
if( typeof fn !== 'function' ){
throw Error('no function provided');
}
return function curriedFn (...args) {
if(args.length < fn.length){
return function () {
return curriedFn.apply(null,args.concat([].slice.call(arguments)));
}
}
return fn.apply(null,args);
}
}
let add = function (x,y,z,o,p) {
return x+y+z+o+p
}
curry(add)(2)(3)(4)(5)(6) // => 20
應(yīng)用場景:
// 封裝調(diào)試console
const loggerHelper = (mode,initialMsg,errorMsg,lineNo) => {
if(mode === 'DeBug'){
console.debug(initialMsg,errorMsg+"at line:"+lineNo)
}else if(mode === 'Log'){
console.log(initialMsg,errorMsg+"at line:"+lineNo)
}else {
throw Error("Wrong mode")
}
}
// 利用柯里化轉(zhuǎn)化
let debugLogger = curryN(loggerHelper)('Log')("DeBug at stats.js")
debugLogger('debug message',123) // => DeBug at stats.js debug messageat line:123
// 在數(shù)組內(nèi)容中查找數(shù)字
let match = curryN(function (expr,str) {
return str.match(expr)
})
let hasNumber = match(/[0-9]+/)
let filter = curryN(function (fn,arr) {
return arr.filter(fn)
})
let findNumbersInArray = filter(hasNumber)
findNumbersInArray(['is','number2']) // =>['number2']
// 求數(shù)組的平方
let map = function (x) {
return x*x
}
let squareAll = curryN(function (fn,arr) {
return arr.map(fn)
})
console.log(squareAll(map)([2,3,4]))
>>偏函數(shù)(partial):
產(chǎn)生背景:curry函數(shù)應(yīng)用參數(shù)列表的順序是從左到最右秋秤,但有時候我們需要顛倒某兩個參數(shù)的順序
const partial = function (fn,...partialArgs) {
let args = partialArgs;
return function (...fullArguments) {
let arg = 0;
for (let i=0; i<args.length && arg<fullArguments.length; i++){
if(args[i] === undefined){
args[i] = fullArguments[arg++]
}
}
return fn.apply(null,args);
}
}
應(yīng)用場景:
// 定時執(zhí)行
let delayTenMsg = partial(setTimeout,undefined,10)
delayTenMsg(()=>{
console.log('執(zhí)行結(jié)束')
})
partial應(yīng)用于任何含有多個參數(shù)的函數(shù):
let obj = {
foo: 'bar',
bar: 'foo'
}
JSON.stringify(obj,null,2)
// 偏函數(shù)改造:
let prettyPrintJSON = partial(JSON.stringify,undefined,null,2)
prettyPrintJSON(obj)
注:這偏函數(shù)實(shí)現(xiàn)中有一個小bug,只能是一次性的,因?yàn)槲覀兪怯胾ndefined替換partialArgs灼卢,而數(shù)組傳遞的是引用绍哎!因而當(dāng)你第二次傳參let obj2 = { foo: 'bar2', bar: 'foo2' }
打印出來的值還是上一次的obj結(jié)果。
總結(jié):什么時候用curry鞋真,什么時候用partial蛇摸,歸結(jié)于使用場景,curry的傳參必須從左到右依次執(zhí)行灿巧,partial用于解決這種不能從左到右依次執(zhí)行的情況赶袄。
柯里化和偏應(yīng)用是函數(shù)式編程的兩種重要技術(shù),作為一名js程序員抠藕,應(yīng)該在代碼庫中選擇柯里化或偏應(yīng)用直一饿肺。
>>組合和管道:
函數(shù)式組合在函數(shù)式中被稱為組合。
Unix的理念:
每個程序只做好一件事盾似。為了完成一項新的任務(wù)敬辣,重新構(gòu)建要好于在復(fù)雜的舊程序中添加新屬性;
每個程序的輸出應(yīng)該是另一個尚未知的程序輸入
管道( | )在兩個命令之間扮演了橋梁的角色
基礎(chǔ)函數(shù)需要遵循如下規(guī)則:每一個基礎(chǔ)函數(shù)都需要接收一個參數(shù)并返回數(shù)據(jù)零院!
組合函數(shù)的真正優(yōu)勢在于:無須創(chuàng)建新的函數(shù)就可以通過基礎(chǔ)函數(shù)解決眼前的問題溉跃。
compose函數(shù)定義:
- 兩元組合函數(shù):
const compose = (a,b)=> c =>a(b(c))
應(yīng)用:
let number = compose(Math.round,parseFloat)
number('6.63') // =>7
let splitIntoSpace = (str) => str.split(" ")
let count = arr => arr.length
let countWords = compose(count,splitIntoSpace)
countWords("hello i am your dad") // =>5
- 多元組合函數(shù):
const composeN = (...fns) => value => {
let _fns = fns.reverse()
return _fns.reduce((acc,fn)=>fn(acc),value)
}
應(yīng)用:
let splitIntoSpace = (str) => str.split(" ")
let count = arr => arr.length
let addOrEven = num => num%2 == 1 ? 'even' : 'add' // 判斷奇數(shù)還是偶數(shù)
let countWords = composeN(addOrEven,count,splitIntoSpace)
composeN(addOrEven,count,splitIntoSpace) // => "even"
pipe函數(shù)定義:
與compose函數(shù)所作的事請相同,只不過交換了數(shù)據(jù)流的方向告抄;
pipe和compose在現(xiàn)實(shí)開發(fā)中應(yīng)該二選一撰茎,否則會讓開發(fā)者感到困擾。
const pipe = (...fns) => value => {
return fns.reduce((acc,fn)=>fn(acc),value)
}
組合滿足結(jié)合律:
composeN(f, composeN(g,h)) = composeN(composeN(f, g),h)
identity函數(shù)定義:
用于定位組合函數(shù)的數(shù)據(jù)流中哪個出錯
const identity = (it) =>{
console.log(it)
return it
}
let countWords2 = composeN(addOrEven,count,identity,splitIntoSpace)
// => ["hello", "i", "am", "your", "dad"]
// => "even"
>>函子:
(錯誤處理)
函子打洼,它將用一種純函數(shù)式的方式幫助我們處理錯誤龄糊;
函子是一個普通對象(在其他語言中,可能是一個類)募疮,它實(shí)現(xiàn)了map函數(shù)炫惩,在遍歷每對象值得時候生成一個新對象。
const Container = function (value) {
this.value = value
}
Container.of = function(val){
return new Container(val)
}
// 函子實(shí)現(xiàn) map方法:
Container.prototype.map = function (fn) {
return Container.of(fn(this.value))
}
Container.of({a:3}) // =>Container {value: {a:3}}
Container.of([1,2,3]) // =>Container {value: [1,2,3]}
Container.of(3).map(e=>e+1) // =>Container {value: 4}
支持鏈?zhǔn)讲僮鳎?/p>
Container.of(3).map(e=>e+1).map(e=>e*e) // =>16
換句話講:函子是一個實(shí)現(xiàn)了map契約的對象阿浓!
MayBe函子:
利用函數(shù)式編程技術(shù)處理錯誤或異常的問題他嚷;
任何層級的鏈?zhǔn)絤ap都會被調(diào)用(管你傳了null還undefined)該過程將連接到鏈條中的最后一個map函數(shù)被調(diào)用。
const MayBe = function (value) {
this.value = value
}
MayBe.of = function(val){
return new MayBe(val)
}
MayBe.prototype.isNothing = function () {
return (this.value === null || this.value === undefined)
}
// 函子實(shí)現(xiàn)map方法:
MayBe.prototype.map = function (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value))
}
// 應(yīng)用
MayBe.of('Roy is a man').map(e=>e.toUpperCase()).map(e=> "Mr. " + e) // MayBe {value: "Mr. ROY IS A MAN"}
拓展:ES5的類為什么不可以用箭頭函數(shù)芭毙,因?yàn)槠洳痪哂衃[constructor]]和prototype屬性
Either函子:
Either的出現(xiàn)是為了解決MayBe不能返回undefined或null值執(zhí)行失敗的狀態(tài)
const Nothing = function (value) {
this.value = value
}
Nothing.of = function(val){
return new Nothing(val)
}
Nothing.prototype.map = function (fn) {
return this // 返回對象本身
}
const Some = function (value) {
this.value = value
}
Some.of = function(val){
return new Some(val)
}
Some.prototype.map = function (fn) {
return Some.of(fn(this.value))
}
// Either定義
const Either = {
Some: Some,
Nothing: Nothing
}
(案例代碼見代碼清單8-14)
Pointed函子:
函子只是一個實(shí)現(xiàn)了map契約的接口
Pointed函子是一個函子的子集筋蓖,它具有實(shí)現(xiàn)了of契約的接口
ES6增加了Array.of,這使數(shù)組成為一個Pointed函子稿蹲!
Array.of("you are a pointed functor,too?") // =>["you are a pointed functor,too?"]
Monad函子:
含有chain的Pointed函子被稱為Monad函子扭勉。
- join解決問題
通過join解決問題鹊奖,解決map深層次嵌套問題
為MayBe函子添加一個join方法:將嵌套的結(jié)構(gòu)展開為一個單一層級
MayBe.prototype.join = function () {
return this.isNothing() ? MayBe.of(null) : this.value
}
// join方法雖很簡單苛聘,但卻能幫助我們打開嵌套的MayBe
let joinExample = MayBe.of(MayBe.of(5)) // => MayBe { value : MayBe { value : 5} }
joinExample.join() // => MayBe { value : 5}
-
實(shí)現(xiàn)chain
我們總是要在map后調(diào)用join,下面把邏輯封裝在一個名為chain的方法中。
MayBe.prototype.chain = function (fn) {
return this.map(fn).join
}
總結(jié):重復(fù)的map調(diào)用會導(dǎo)致嵌套問題设哗,chain能幫助扁平化MayBe數(shù)據(jù)
>>Generator
作用:用generator的next調(diào)用替換回調(diào)
// 這里的業(yè)務(wù)場景:先通過接口1獲取picturesJson數(shù)據(jù)唱捣,然后通過picturesJson數(shù)據(jù)的url字段,再次請求接口2网梢,最終獲得第一張圖的數(shù)據(jù)
function request(url) {
httpGetAsync(url,function (reponse) {
generator.next(reponse)
})
}
function *main() {
let picturesJson = yield request('https://www.xxx.com/pics/.json')
let firstPictureData = yield request(picturesJson.data.children[0].data.url + '.json')
console.log(firstPictureData) // 最后成功打印結(jié)果
}
// 執(zhí)行
let generator = main()
generator.next()
通過Generator解決了地獄回調(diào)問題震缭。
思考總結(jié):Generator的應(yīng)用場景主要用于解決某個數(shù)據(jù)需要等到上一個異步請求結(jié)果,早期這種情況战虏,我們會選擇回調(diào)的方法解決拣宰,但是如果遇到多個回調(diào)嵌套的問題,就會造成代碼不好維護(hù)(地獄回調(diào))烦感,選擇Generator替代回調(diào)巡社,代碼會變得更加直觀更好維護(hù)。