摘要:函數式編程的定義函數是一段可以通過其名稱被調用的代碼。純函數大多數函數式編程的好處來自于編寫純函數,純函數是對給定的輸入返回相同的輸出的函數,并且純函數不應依賴任何外部變量,也不應改變任何外部變量。
一個持續(xù)更新的github筆記,鏈接地址:Front-End-Basics,可以watch,也可以star。
此篇文章的地址:JavaScript函數式編程入門經典
正文開始
什么是函數式編程?為何它重要? 數學中的函數f(x) = y // 一個函數f,以x為參數,并返回輸出y
關鍵點:
函數必須總是接受一個參數
函數必須總是返回一個值
函數應該依據接收到的參數(例如x)而不是外部環(huán)境運行
對于一個給定的x,只會輸出唯一的一個y
函數式編程技術主要基于數學函數和它的思想,所以要理解函數式編程,先了解數學函數是有必要的。
函數式編程的定義函數是一段可以通過其名稱被調用的代碼。它可以接受參數,并返回值。
與面向對象編程(Object-oriented programming)和過程式編程(Procedural programming)一樣,函數式編程(Functional programming)也是一種編程范式。我們能夠以此創(chuàng)建僅依賴輸入就可以完成自身邏輯的函數。這保證了當函數被多次調用時仍然返回相同的結果(引用透明性)。函數不會改變任何外部環(huán)境的變量,這將產生可緩存的,可測試的代碼庫。
函數式編程具有以下特征 1、引用透明性所有的函數對于相同的輸入都將返回相同的值,函數的這一屬性被稱為引用透明性(Referential Transparency)
// 引用透明的例子,函數identity無論輸入什么,都會原封不動的返回 var identity = (i) => {return i}
把一個引用透明的函數用于其他函數調用之間。
sum(4,5) + identity(1)
根據引用透明的定義,我們可以把上面的語句換成:
sum(4,5) + 1
該過程被稱為替換模型(Substitution Model),因為函數的邏輯不依賴其他全局變量,你可以直接替換函數的結果,這與它的值是一樣的。所以,這使得并發(fā)代碼和緩存成為可能。
并發(fā)代碼: 并發(fā)運行的時候,如果依賴了全局數據,要保證數據一致,必須同步,而且必要時需要鎖機制。遵循引用透明的函數只依賴參數的輸入,所以可以自由的運行。
緩存: 由于函數會為給定的輸入返回相同的值,實際上我們就能緩存它了。比如實現(xiàn)一個計算給定數值的階乘的函數,我們就可以把每次階乘的結果緩存下來,下一次直接用,就不用計算了。比如第一次輸入5,結果是120,第二次輸入5,我們知道結果必然是120,所以就可以返回已緩存的值,而不必再計算一次。
2、聲明式和抽象函數式編程主張聲明式編程和編寫抽象的代碼。
// 有一個數組,要遍歷它并把它打印到控制臺 /*命令式*/ var array = [1,2,3] for(var i = 0; i < array.length; i++) console(array[i]) // 打印 1,2,3 // 命令式編程中,我們精確的告訴程序應該“如何”做:獲取數組的長度,通過數組的長度循環(huán)數組,在每一次循環(huán)中用索引獲取每一個數組元素,然后打印出來。 // 但是我們的任務只是打印出數組的元素。并不是要告訴編譯器要如何實現(xiàn)一個遍歷。 /*聲明式*/ var array = [1,2,3] array.forEach((element) => console.log(element)) // 打印 1,2,3 // 我們使用了一個處理“如何”做的抽象函數,然后我們就能只關心做“什么”了
大多數函數式編程的好處來自于編寫純函數,純函數是對給定的輸入返回相同的輸出的函數,并且純函數不應依賴任何外部變量,也不應改變任何外部變量。
純函數的好處純函數產生容易測試的代碼
純函數容易寫出合理的代碼
純函數容易寫出并發(fā)代碼
純函數總是允許我們并發(fā)的執(zhí)行代碼。因為純函數不會改變它的環(huán)境,這意味著我們根本不需要擔心同步問題。
純函數的輸出結果可緩存
既然純函數總是為給定的輸入返回相同的輸出,那么我們就能夠緩存函數的輸出。
高階函數 數據和數據類型程序作用于數據,數據對于程序的執(zhí)行很重要。每種編程語言都有數據類型。這些數據類型能夠存儲數據并允許程序作用其中。
JavaScript中函數是一等公民(First Class Citizens)當一門語言允許函數作為任何其他數據類型使用時,函數被稱為一等公民。也就是說函數可被賦值給變量,作為參數傳遞,也可被其他函數返回。
函數作為JavaScript的一種數據類型,由于函數是類似String的數據類型,所以我們能把函數存入一個變量,能夠作為函數的參數進行傳遞。所以JavaScript中函數是一等公民。
高階函數的定義接受另一個函數作為其參數的函數稱為高階函數(Higher-Order-Function),或者說高階函數是接受函數作為參數并且/或者返回函數作為輸出的函數。
抽象和高階函數一般而言,高階函數通常用于抽象通用的問題,換句話說,高階函數就是定義抽象。
抽象 : 在軟件工程和計算機科學中,抽象是一種管理計算機系統(tǒng)復雜性的技術。 通過建立一個人與系統(tǒng)進行交互的復雜程度,把更復雜的細節(jié)抑制在當前水平之下。簡言之,抽象讓我們專注于預定的目標而無須關心底層的系統(tǒng)概念。
例如:你在編寫一個涉及數值操作的代碼,你不會對底層硬件的數字表現(xiàn)方式到底是16位還是32位整數有很深的了解,包括這些細節(jié)在哪里屏蔽。因為它們被抽象出來了,只留下了簡單的數字給我們使用。
// 用forEach抽象出遍歷數組的操作 const forEach = (array,fn) => { let i; for(i=0;i閉包和高階函數console.log(data))
什么是閉包?簡言之,閉包就是一個內部函數。什么是內部函數?就是在另一個函數內部的函數。
閉包的強大之處在于它對作用域鏈(或作用域層級)的訪問。從技術上講,閉包有3個可訪問的作用域。
(1) 在它自身聲明之內聲明的變量
(2) 對全局變量的訪問
(3) 對外部函數變量的訪問(關鍵點)
實例一:假設你再遍歷一個來自服務器的數組,并發(fā)現(xiàn)數據錯了。你想調試一下,看看數組里面究竟包含了什么。不要用命令式的方法,要用函數式的方法來實現(xiàn)。這里就需要一個 tap 函數。
const tap = (value) => { return (fn) => { typeof fn === "function" && fn(value) console.log(value) } } // 沒有調試之前 forEach(array, data => { console.log(data + data) }) // 在 forEach 中使用 tap 調試 forEach(array, data => { tap(data)(() => { console.log(data + data) }) })
完成一個簡單的reduce函數
const reduce = (array,fn,initialValue) => { let accumulator; if(initialValue != undefined) accumulator = initialValue else accumulator = array[0] if(initialValue === undefined) for(let i = 1; i < array.length; i++) accumulator = fn(accumulator, array[i]) else for(let value of array) accumulator = fn(accumulator,value) return accumulator } console.log(reduce([1,2,3], (accumulator,value) => accumulator + value)) // 打印出6柯里化與偏應用 一些概念 一元函數
只接受一個參數的函數稱為一元(unary)函數。
二元函數只接受兩個參數的函數稱為二元(binary)函數。
變參函數變參函數是接受可變數量的函數。
柯里化柯里化是把一個多參數函數轉換為一個嵌套的一元函數的過程。
例如
// 一個多參數函數 const add = (x,y) => x + y; add(2,3) // 一個嵌套的一元函數 const addCurried = x => y => x + y; addCurried(2)(3) // 然后我們寫一個高階函數,把 add 轉換成 addCurried 的形式。 const curry = (binaryFn) => { return function (firstArg) { return function (secondArg) { return binaryFn(firstArg,secondArg) } } } let autoCurriedAdd = carry(add) autoCurriedAdd(2)(3)
上面只是簡單實現(xiàn)了一個二元函數的柯里化,下面我們要實現(xiàn)一個更多參數的函數的柯里化。
const curry = (fn) => { if (typeof fn !== "function") { throw Error("No function provided") } return function curriedFn (...args) { // 判斷當前接受的參數是不是小于進行柯里化的函數的參數個數 if(args.length < fn.length) { // 如果小于的話就返回一個函數再去接收剩下的參數 return function (...argsOther) { return curriedFn.apply(null, args.concat(argsOther)) } }else { return fn.apply(null,args) } } } const multiply = (x,y,z) => x * y * z; console.log(curry(multiply)(2)(3)(4))
柯里化的應用實例:從數組中找出含有數字的元素
let match = curry(function (expr,str) { return str.match(expr) }) let hasNumber = match(/[0-9]+/) let initFilter = curry(function (fn,array) { return array.filter(fn) }) let findNumberInArray = initFilter(hasNumber) console.log(findNumberInArray(["aaa", "bb2", "33c", "ffffd", ])) // 打印 [ "bb2", "33c" ]偏應用
我們上面設計的柯里化函數總是在最后接受一個數組,這使得它能接受的參數列表只能是從最左到最右。
但是有時候,我們不能按照從左到右的這樣嚴格傳入參數,或者只是想部分地應用函數參數。這里我們就需要用到偏應用這個概念,它允許開發(fā)者部分地應用函數參數。
const partial = function (fn, ...partialArgs) { return function (...fullArguments) { let args = partialArgs 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) } }
偏應用的示例:
// 打印某個格式化的JSON let prettyPrintJson = partial(JSON.stringify,undefined,null,2) console.log(prettyPrintJson({name:"fangxu",gender:"male"})) // 打印出 { "name": "fangxu", "gender": "male" }組合與管道 Unix的理念
每個程序只做好一件事情,為了完成一項新的任務,重新構建要好于在復雜的舊程序中添加新“屬性”。
每個程序的輸出應該是另一個尚未可知的程序的輸入。
每一個基礎函數都需要接受一個參數并返回數據。
組合(compose)const compose = (...fns) => { return (value) => reduce(fns.reverse(),(acc,fn) => fn(acc), value) }
compose 組合的函數,是按照傳入的順序從右到左調用的。所以傳入的 fns 要先 reverse 一下,然后我們用到了reduce ,reduce 的累加器初始值是 value ,然后會調用 (acc,fn) => fn(acc), 依次從 fns 數組中取出 fn ,將累加器的當前值傳入 fn ,即把上一個函數的返回值傳遞到下一個函數的參數中。
組合的實例:
let splitIntoSpace = (str) => str.split(" ") let count = (array) => array.length const countWords = composeN(count, splitIntoSpace) console.log(countWords("make smaller or less in amount")) // 打印 6管道/序列
compose 函數的數據流是從右往左的,最右側的先執(zhí)行。當然,我們還可以讓最左側的函數先執(zhí)行,最右側的函數最后執(zhí)行。這種從左至右處理數據流的過程稱為管道(pipeline)或序列(sequence)。
// 跟compose的區(qū)別,只是沒有調用fns.reverse() const pipe = (...fns) => (value) => reduce(fns,(acc,fn) => fn(acc),value)函子 什么是函子(Functor)?
定義:函子是一個普通對象(在其它語言中,可能是一個類),它實現(xiàn)了map函數,在遍歷每個對象值的時候生成一個新對象。
實現(xiàn)一個函子1、簡言之,函子是一個持有值的容器。而且函子是一個普通對象。我們就可以創(chuàng)建一個容器(也就是對象),讓它能夠持有任何傳給它的值。
const Container = function (value) { this.value = value } let testValue = new Container(1) // => Container {value:1}
我們給 Container 增加一個靜態(tài)方法,它可以為我們在創(chuàng)建新的 Containers 時省略 new 關鍵字。
Container.of = function (value) { return new Container(value) } // 現(xiàn)在我們就可以這樣來創(chuàng)建 Container.of(1) // => Container {value:1}
2、函子需要實現(xiàn) map 方法,具體的實現(xiàn)是,map 函數從 Container 中取出值,傳入的函數把取出的值作為參數調用,并將結果放回 Container。
為什么需要 map 函數,我們上面實現(xiàn)的 Container 僅僅是持有了傳給它的值。但是持有值的行為幾乎沒有任何應用場景,而 map 函數發(fā)揮的作用就是,允許我們使用當前 Container 持有的值調用任何函數。
Container.prototype.map = function (fn) { return Container.of(fn(this.value)) } // 然后我們實現(xiàn)一個數字的 double 操作 let double = (x) => x + x; Container.of(3).map(double) // => Container {value: 6}
3、map返回了一傳入函數的執(zhí)行結果為值的 Container 實例,所以我們可以鏈式操作。
Container.of(3).map(double).map(double).map(double) // => Container {value: 24}
通過以上的實現(xiàn),我們可以發(fā)現(xiàn),函子就是一個實現(xiàn)了map契約的對象。函子是一個尋求契約的概念,該契約很簡單,就是實現(xiàn) map 。根據實現(xiàn) map 函數的方式不同,會產生不同類型的函子,如 MayBe 、 Either
函子可以用來做什么?之前我們用tap函數來函數式的解決代碼報錯的調試問題,如何更加函數式的處理代碼中的問題,那就需要用到下面我們說的MayBe函子
MayBe 函子讓我們先寫一個upperCase函數來假設一種場景
let value = "string"; function upperCase(value) { // 為了避免報錯,我們得寫這么一個判斷 if(value != null || value != undefined) return value.toUpperCase() } upperCase(value) // => STRING
如上面所示,我們代碼中經常需要判斷一些null和undefined的情況。下面我們來看一下MayBe函子的實現(xiàn)。
// MayBe 跟上面的 Container 很相似 export const MayBe = function (value) { this.value = value } MayBe.of = function (value) { return new MayBe(value) } // 多了一個isNothing MayBe.prototype.isNoting = function () { return this.value === null || this.value === undefined; } // 函子必定有 map,但是 map 的實現(xiàn)方式可能不同 MayBe.prototype.map = function(fn) { return this.isNoting()?MayBe.of(null):MayBe.of(fn(this.value)) } // MayBe應用 let value = "string"; MayBe.of(value).map(upperCase) // => MayBe { value: "STRING" } let nullValue = null MayBe.of(nullValue).map(upperCase) // 不會報錯 MayBe { value: null }Either 函子
MayBe.of("tony") .map(() => undefined) .map((x)f => "Mr. " + x)
上面的代碼結果是 MyaBe {value: null},這只是一個簡單的例子,我們可以想一下,如果代碼比較復雜,我們是不知道到底是哪一個分支在檢查 undefined 和 null 值時執(zhí)行失敗了。這時候我們就需要 Either 函子了,它能解決分支拓展問題。
const Nothing = function (value) { this.value = value; } Nothing.of = function (value) { return new Nothing(value) } Nothing.prototype.map = function (fn) { return this; } const Some = function (value) { this.value = value; } Some.of = function (value) { return new Some(value) } Some.prototype.map = function (fn) { return Some.of(fn(this.value)); } const Either = { Some, Nothing }Pointed 函子
函子只是一個實現(xiàn)了 map 契約的接口。Pointed 函子也是一個函子的子集,它具有實現(xiàn)了 of 契約的接口。 我們在 MayBe 和 Either 中也實現(xiàn)了 of 方法,用來在創(chuàng)建 Container 時不使用 new 關鍵字。所以 MayBe 和 Either 都可稱為 Pointed 函子。
ES6 增加了 Array.of, 這使得數組成為了一個 Pointed 函子。Monad 函子
MayBe 函子很可能會出現(xiàn)嵌套,如果出現(xiàn)嵌套后,我們想要繼續(xù)操作真正的value是有困難的。必須深入到 MayBe 內部進行操作。
let joinExample = MayBe.of(MayBe.of(5)); // => MayBe { value: MayBe { value: 5 } } // 這個時候我們想讓5加上4,需要深入 MayBe 函子內部 joinExample.map((insideMayBe) => { return insideMayBe.map((value) => value + 4) }) // => MayBe { value: MayBe { value: 9 } }
我們這時就可以實現(xiàn)一個 join 方法來解決這個問題。
// 如果通過 isNothing 的檢查,就返回自身的 value MayBe.prototype.join = function () { return this.isNoting()? MayBe.of(null) : this.value }
let joinExample2 = MayBe.of(MayBe.of(5)); // => MayBe { value: MayBe { value: 5 } } // 這個時候我們想讓5加上4就很簡單了。 joinExample2.join().map((value) => value + 4) // => MayBe { value: 9 }
再延伸一下,我們擴展一個 chain 方法。
MayBe.prototype.chain = function (fn) { return this.map(fn).join() }
調用 chain 后就能把嵌套的 MayBe 展開了。
let joinExample3 = MayBe.of(MayBe.of(5)); // => MayBe { value: MayBe { value: 5 } } joinExample3.chain((insideMayBe) => { return insideMayBe.map((value) => value + 4) }) // => MayBe { value: 9 }
Monad 其實就是一個含有 chain 方法的函子。只有of 和 map 的 MayBe 是一個函子,含有 chain 的函子是一個 Monad。
總結 JavaScript是函數式編程語言嗎?函數式編程主張函數必須接受至少一個參數并返回一個值,但是JavaScript允許我們創(chuàng)建一個不接受參數并且實際上什么也不返回的函數。所以JavaScript不是一種純函數語言,更像是一種多范式的語言,不過它非常適合函數式編程范式。
補充 1、純函數是數學函數function generateGetNumber() { let numberKeeper = {} return function (number) { return numberKeeper.hasOwnProperty(number) ? number : numberKeeper[number] = number + number } } const getNumber = generateGetNumber() getNumber(1) getNumber(2) …… getNumber(9) getNumber(10) // 此時numberKeeper為: { 1: 2 2: 4 3: 6 4: 8 5: 10 6: 12 7: 14 8: 16 9: 18 10: 20 }
現(xiàn)在我們規(guī)定,getNumber只接受1-10范圍的參數,那么返回值肯定是 numberKeeper 中的某一個 value 。據此我們分析一下 getNumber ,該函數接受一個輸入并為給定的范圍(此處范圍是10)映射輸出。輸入具有強制的、相應的輸出,并且也不存在映射兩個輸出的輸入。
下面我來再看一下數學函數的定義(維基百科)
在數學中,函數是一種輸入集合和可允許的輸出集合之間的關系,具有如下屬性:每個輸入都精確地關聯(lián)一個輸出。函數的輸入稱為參數,輸出稱為值。對于一個給定的函數,所有被允許的輸入集合稱為該函數的定義域,而被允許的輸出集合稱為值域。
根據我們對于 getNumber 的分析,對照數學函數的定義,會發(fā)現(xiàn)完全一致。我們上面的getNumber函數的定義域是1-10,值域是2,4,6,……18,20
2、實例文中所有的概念對應的實例可以在 https://github.com/qiqihaobenben/learning-functional 獲取,可以打開對應的注釋來實際執(zhí)行一下。
3、薦書《JavaScript ES6 函數式編程入門經典》,強烈建議想入門函數式編程的同學看一下,書有點老,可以略過工具介紹之類的,關鍵看其內在的思想,最重要的是,這本書很薄,差不多跟一本漫畫書類似。
4、推薦文章(非引用文章)漫談 JS 函數式編程(一)
從一道坑人的面試題說函數式編程
函數式編程入門教程
函數式編程的一點實戰(zhàn)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/104113.html
摘要:本文與大家分享一些編程語言的入門書籍,其中不乏經典。全書貫穿的主體是如何思考設計開發(fā)的方法,而具體的編程語言,只是提供一個具體場景方便介紹的媒介。入門入門容易理解而且讀起來幽默風趣,對于編程初學者和語言新手而言是理想的書籍。 本文與大家分享一些Python編程語言的入門書籍,其中不乏經典。我在這里分享的,大部分是這些書的英文版,如果有中文版的我也加上了。有關書籍的介紹,大部分截取自是官...
摘要:書籍如下面向對象編程指南,風格輕松易懂,比較適合初學者,原型那塊兒講得透徹,種繼承方式呢。還有另一件事情是,比如發(fā)現(xiàn)自己某個知識點不太清楚,可以單獨去百度。 作者:小不了鏈接:https://zhuanlan.zhihu.com/p/...來源:知乎著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。 鑒于時不時,有同學私信問我(老姚,下同)怎么學前端的問題。這里統(tǒng)一回...
摘要:特意對前端學習資源做一個匯總,方便自己學習查閱參考,和好友們共同進步。 特意對前端學習資源做一個匯總,方便自己學習查閱參考,和好友們共同進步。 本以為自己收藏的站點多,可以很快搞定,沒想到一入匯總深似海。還有很多不足&遺漏的地方,歡迎補充。有錯誤的地方,還請斧正... 托管: welcome to git,歡迎交流,感謝star 有好友反應和斧正,會及時更新,平時業(yè)務工作時也會不定期更...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。異步編程入門的全稱是前端經典面試題從輸入到頁面加載發(fā)生了什么這是一篇開發(fā)的科普類文章,涉及到優(yōu)化等多個方面。 TypeScript 入門教程 從 JavaScript 程序員的角度總結思考,循序漸進的理解 TypeScript。 網絡基礎知識之 HTTP 協(xié)議 詳細介紹 HTT...
摘要:前言月份開始出沒社區(qū),現(xiàn)在差不多月了,按照工作的說法,就是差不多過了三個月的試用期,準備轉正了一般來說,差不多到了轉正的時候,會進行總結或者分享會議那么今天我就把看過的一些學習資源主要是博客,博文推薦分享給大家。 1.前言 6月份開始出沒社區(qū),現(xiàn)在差不多9月了,按照工作的說法,就是差不多過了三個月的試用期,準備轉正了!一般來說,差不多到了轉正的時候,會進行總結或者分享會議!那么今天我就...
閱讀 3782·2021-09-22 15:49
閱讀 3321·2021-09-08 09:35
閱讀 1431·2019-08-30 15:55
閱讀 2333·2019-08-30 15:44
閱讀 726·2019-08-29 16:59
閱讀 1610·2019-08-29 16:16
閱讀 492·2019-08-28 18:06
閱讀 904·2019-08-27 10:55