摘要:今天這篇文章主要介紹函數(shù)式編程的思想。函數(shù)式編程通過(guò)最小化變化使得代碼更易理解。在函數(shù)式編程里面,組合是一個(gè)非常非常非常重要的思想。可以看到函數(shù)式編程在開(kāi)發(fā)中具有聲明模式。而函數(shù)式編程旨在盡可能的提高代碼的無(wú)狀態(tài)性和不變性。
最開(kāi)始接觸函數(shù)式編程的時(shí)候是在小米工作的時(shí)候,那個(gè)時(shí)候看老大以前寫(xiě)的代碼各種 compose,然后一些 ramda 的一些工具函數(shù),看著很吃力,然后極力吐槽函數(shù)式編程,現(xiàn)在回想起來(lái),那個(gè)時(shí)候的自己真的是見(jiàn)識(shí)短淺,只想說(shuō),"真香"。
最近在研究函數(shù)式編程,真的是在學(xué)習(xí)的過(guò)程中感覺(jué)自己的思維提升了很多,抽象能力大大的提高了,讓我深深的感受到了函數(shù)式編程的魅力。所以我打算后面用 5 到 8 篇的篇幅,詳細(xì)的介紹一下函數(shù)式編程的思想,基礎(chǔ)、如何設(shè)計(jì)、測(cè)試等。
今天這篇文章主要介紹函數(shù)式編程的思想。
函數(shù)式編程有用嗎?
什么是函數(shù)式編程?
函數(shù)式編程的優(yōu)點(diǎn)。
面向?qū)ο缶幊?OOP)通過(guò)封裝變化使得代碼更易理解。
函數(shù)式編程(FP)通過(guò)最小化變化使得代碼更易理解。
-- Michacel Feathers(Twitter)
總所周知 JavaScript 是一種擁有很多共享狀態(tài)的動(dòng)態(tài)語(yǔ)言,慢慢的,代碼就會(huì)積累足夠的復(fù)雜性,變得笨拙難以維護(hù)。面向?qū)ο笤O(shè)計(jì)能幫我們?cè)谝欢ǔ潭壬辖鉀Q這個(gè)問(wèn)題,但是還不夠。
由于有很多的狀態(tài),所以處理數(shù)據(jù)流和變化的傳遞顯得尤為重要,不知道你們知道響應(yīng)式編程與否,這種編程范式有助于處理 JavaScript 的異步或者事件響應(yīng)??傊?,當(dāng)我們?cè)谠O(shè)計(jì)應(yīng)用程序的時(shí)候,我們應(yīng)該考慮是否遵守了以下的設(shè)計(jì)原則。
可擴(kuò)展性--我是否需要不斷地重構(gòu)代碼來(lái)支持額外的功能?
易模塊化--如果我更改了一個(gè)文件,另一個(gè)文件是否會(huì)受到影響?
可重用性--是否有很多重復(fù)的代碼?
可測(cè)性--給這些函數(shù)添加單元測(cè)試是否讓我糾結(jié)?
易推理性--我寫(xiě)的代碼是否非結(jié)構(gòu)化嚴(yán)重并難以推理?
我這能這么跟你說(shuō),一旦你學(xué)會(huì)了函數(shù)式編程,這些問(wèn)題迎刃而解,本來(lái)函數(shù)式編程就是這個(gè)思想,一旦你掌握了函數(shù)式,然后你再學(xué)習(xí)響應(yīng)式編程那就比較容易懂了,這是我親身體會(huì)的。我之前在學(xué) Rxjs 的時(shí)候是真的痛苦,說(shuō)實(shí)話,Rxjs 是我學(xué)過(guò)最難的庫(kù)了,沒(méi)有之一。在經(jīng)歷過(guò)痛苦的一兩個(gè)月之后,有些東西還是不能融會(huì)貫通,知道我最近研究函數(shù)式編程,才覺(jué)得是理所當(dāng)然。毫無(wú)夸張,我也盡量在后面的文章中給大家介紹一下 Rxjs,這個(gè)話題我也在公司分享過(guò)。
什么是函數(shù)式編程?簡(jiǎn)單來(lái)說(shuō),函數(shù)式編程是一種強(qiáng)調(diào)以函數(shù)使用為主的軟件開(kāi)發(fā)風(fēng)格。看到這句我想你還是一臉懵逼,不知道函數(shù)式編程是啥,不要著急,看到最后我相信你會(huì)明白的。
還有一點(diǎn)你要記住,函數(shù)式編程的目的是使用函數(shù)來(lái)抽象作用在數(shù)據(jù)之上的控制流和操作,從而在系統(tǒng)中消除副作用并減少對(duì)狀態(tài)的改變。
下面我們通過(guò)例子來(lái)簡(jiǎn)單的演示一下函數(shù)式編程的魅力。
現(xiàn)在的需求就是輸出在網(wǎng)頁(yè)上輸出 “Hello World”。
可能初學(xué)者會(huì)這么寫(xiě)。
document.querySelector("#msg").innerHTML = "Hello World
"
這個(gè)程序很簡(jiǎn)單,但是所有代碼都是死的,不能重用,如果想改變消息的格式、內(nèi)容等就需要重寫(xiě)整個(gè)表達(dá)式,所以可能有經(jīng)驗(yàn)的前端開(kāi)發(fā)者會(huì)這么寫(xiě)。
function printMessage(elementId, format, message) { document.querySelector(elementId).innerHTML = `<${format}>${message}${format}>` } printMessage("msg", "h1", "Hello World")
這樣確實(shí)有所改進(jìn),但是任然不是一段可重用的代碼,如果是要將文本寫(xiě)入文件,不是非 HTML,或者我想重復(fù)的顯示 Hello World。
那么作為一個(gè)函數(shù)式開(kāi)發(fā)者會(huì)怎么寫(xiě)這段代碼呢?
const printMessage = compose(addToDom("msg", h1, echo)) printMessage("Hello World")
解釋一下這段代碼,其中的 h1 和 echo 都是函數(shù),addToDom 很明顯也能看出它是函數(shù),那么我們?yōu)槭裁匆獙?xiě)成這樣呢?看起來(lái)多了很多函數(shù)一樣。
其實(shí)我們是講程序分解為一些更可重用、更可靠且更易于理解的部分,然后再將他們組合起來(lái),形成一個(gè)更易推理的程序整體,這是我們前面談到的基本原則。
compose 簡(jiǎn)單解釋一下,他會(huì)讓函數(shù)從最后一個(gè)參數(shù)順序執(zhí)行到第一個(gè)參數(shù),compose 的每個(gè)參數(shù)都是函數(shù),不明白的可以查一下,在 redux 的中間件部分這個(gè)函數(shù)式精華。
可以看到我們是將一個(gè)任務(wù)拆分成多個(gè)最小顆粒的函數(shù),然后通過(guò)組合的方式來(lái)完成我們的任務(wù),這跟我們組件化的思想很類(lèi)似,將整個(gè)頁(yè)面拆分成若干個(gè)組件,然后拼裝起來(lái)完成我們的整個(gè)頁(yè)面。在函數(shù)式編程里面,組合是一個(gè)非常非常非常重要的思想。
好,我們現(xiàn)在再改變一下需求,現(xiàn)在我們需要將文本重復(fù)三遍,打印到控制臺(tái)。
var printMessaage = compose(console.log, repeat(3), echo) printMessage(‘Hello World’)
可以看到我們更改了需求并沒(méi)有去修改內(nèi)部邏輯,只是重組了一下函數(shù)而已。
可以看到函數(shù)式編程在開(kāi)發(fā)中具有聲明模式。為了充分理解函數(shù)式編程,我們先來(lái)看下幾個(gè)基本概念。
聲明式編程
純函數(shù)
引用透明
不可變性
聲明式編程函數(shù)式編程屬于聲明是編程范式:這種范式會(huì)描述一系列的操作,但并不會(huì)暴露它們是如何實(shí)現(xiàn)的或是數(shù)據(jù)流如何傳過(guò)它們。
我們所熟知的 SQL 語(yǔ)句就是一種很典型的聲明式編程,它由一個(gè)個(gè)描述查詢結(jié)果應(yīng)該是什么樣的斷言組成,對(duì)數(shù)據(jù)檢索的內(nèi)部機(jī)制進(jìn)行了抽象。
我們?cè)賮?lái)看一組代碼再來(lái)對(duì)比一下命令式編程和聲明式編程。
// 命令式方式 var array = [0, 1, 2, 3] for(let i = 0; i < array.length; i++) { array[i] = Math.pow(array[i], 2) } array; // [0, 1, 4, 9] // 聲明式方式 [0, 1, 2, 3].map(num => Math.pow(num, 2))
可以看到命令式很具體的告訴計(jì)算機(jī)如何執(zhí)行某個(gè)任務(wù)。
而聲明式是將程序的描述與求值分離開(kāi)來(lái)。它關(guān)注如何用各種表達(dá)式來(lái)描述程序邏輯,而不一定要指明其控制流或狀態(tài)關(guān)系的變化。
為什么我們要去掉代碼循環(huán)呢?循環(huán)是一種重要的命令控制結(jié)構(gòu),但很難重用,并且很難插入其他操作中。而函數(shù)式編程旨在盡可能的提高代碼的無(wú)狀態(tài)性和不變性。要做到這一點(diǎn),就要學(xué)會(huì)使用無(wú)副作用的函數(shù)--也稱純函數(shù)
純函數(shù)純函數(shù)指沒(méi)有副作用的函數(shù)。相同的輸入有相同的輸出,就跟我們上學(xué)學(xué)的函數(shù)一樣,常常這些情況會(huì)產(chǎn)生副作用。
改變一個(gè)全局的變量、屬性或數(shù)據(jù)結(jié)構(gòu)
改變一個(gè)函數(shù)參數(shù)的原始值
處理用戶輸入
拋出一個(gè)異常
屏幕打印或記錄日志
查詢 HTML 文檔,瀏覽器的 Cookie 或訪問(wèn)數(shù)據(jù)庫(kù)
舉一個(gè)簡(jiǎn)單的例子
var counter = 0 function increment() { return ++counter; }
這個(gè)函數(shù)就是不純的,它讀取了外部的變量,可能會(huì)覺(jué)得這段代碼沒(méi)有什么問(wèn)題,但是我們要知道這種依賴外部變量來(lái)進(jìn)行的計(jì)算,計(jì)算結(jié)果很難預(yù)測(cè),你也有可能在其他地方修改了 counter 的值,導(dǎo)致你 increment 出來(lái)的值不是你預(yù)期的。
對(duì)于純函數(shù)有以下性質(zhì):
僅取決于提供的輸入,而不依賴于任何在函數(shù)求值或調(diào)用間隔時(shí)可能變化的隱藏狀態(tài)和外部狀態(tài)。
不會(huì)造成超出作用域的變化,例如修改全局變量或引用傳遞的參數(shù)。
但是在我們平時(shí)的開(kāi)發(fā)中,有一些副作用是難以避免的,與外部的存儲(chǔ)系統(tǒng)或 DOM 交互等,但是我們可以通過(guò)將其從主邏輯中分離出來(lái),使他們易于管理。
現(xiàn)在我們有一個(gè)小需求:通過(guò) id 找到學(xué)生的記錄并渲染在瀏覽器(在寫(xiě)程序的時(shí)候要想到可能也會(huì)寫(xiě)到控制臺(tái),數(shù)據(jù)庫(kù)或者文件,所以要想如何讓自己的代碼能重用)中。
// 命令式代碼 function showStudent(id) { // 這里假如是同步查詢 var student = db.get(id) if(student !== null) { // 讀取外部的 elementId document.querySelector(`${elementId}`).innerHTML = `${student.id},${student.name},${student.lastname}` } else { throw new Error("not found") } } showStudent("666") // 函數(shù)式代碼 // 通過(guò) find 函數(shù)找到學(xué)生 var find = curry(function(db, id) { var obj = db.get(id) if(obj === null) { throw new Error("not fount") } return obj }) // 將學(xué)生對(duì)象 format var csv = (student) => `${student.id},${student.name},${student.lastname}` // 在屏幕上顯示 var append = curry(function(elementId, info) { document.querySelector(elementId).innerHTML = info }) var showStudent = compose(append("#student-info"), csv, find(db)) showStudent("666")
如果看不懂 curry (柯里化)的先不著急,這是一個(gè)對(duì)于新手來(lái)說(shuō)比較難理解的一個(gè)概念,在函數(shù)式編程里面起著至關(guān)重要的作用。
可以看到函數(shù)式代碼通過(guò)較少這些函數(shù)的長(zhǎng)度,將 showStudent 編寫(xiě)為小函數(shù)的組合。這個(gè)程序還不夠完美,但是已經(jīng)可以展現(xiàn)出相比于命令式的很多優(yōu)勢(shì)了。
靈活。有三個(gè)可重用的組件
聲明式的風(fēng)格,給高階步驟提供了一個(gè)清晰視圖,增強(qiáng)了代碼的可讀性
另外是將純函數(shù)與不純的行為分離出來(lái)。
我們看到純函數(shù)的輸出結(jié)果是一致的,可預(yù)測(cè)的,相同的輸入會(huì)有相同的返回值,這個(gè)其實(shí)也被稱為引用透明。
引用透明引用透明是定義一個(gè)純函數(shù)較為正確的方法。純度在這個(gè)意義上表面一個(gè)函數(shù)的參數(shù)和返回值之間映射的純的關(guān)系。如果一個(gè)函數(shù)對(duì)于相同的輸入始終產(chǎn)生相同的結(jié)果,那么我們就說(shuō)它是引用透明。
這個(gè)概念很容易理解,簡(jiǎn)單的舉兩個(gè)例子就行了。
// 非引用透明 var counter = 0 function increment() { return ++counter } // 引用透明 var increment = (counter) => counter + 1
其實(shí)對(duì)于箭頭函數(shù)在函數(shù)式編程里面有一個(gè)高大上的名字,叫 lambda 表達(dá)式,對(duì)于這種匿名函數(shù)在學(xué)術(shù)上就是叫 lambda 表達(dá)式,現(xiàn)在在 Java 里面也是支持的。不可變數(shù)據(jù)
不可變數(shù)據(jù)是指那些創(chuàng)建后不能更改的數(shù)據(jù)。與許多其他語(yǔ)言一樣,JavaScript 里有一些基本類(lèi)型(String,Number 等)從本質(zhì)上是不可變的,但是對(duì)象就是在任意的地方可變。
考慮一個(gè)簡(jiǎn)單的數(shù)組排序代碼:
var sortDesc = function(arr) { return arr.sort(function(a, b) { return b - a }) } var arr = [1, 3, 2] sortDesc(arr) // [1, 2, 3] arr // [1, 2, 3]
這段代碼看似沒(méi)什么問(wèn)題,但是會(huì)導(dǎo)致在排序的過(guò)程中會(huì)產(chǎn)生副作用,修改了原始引用,可以看到原始的 arr 變成了 [1, 2, 3]。這是一個(gè)語(yǔ)言缺陷,后面會(huì)介紹如何克服。
總結(jié)使用純函數(shù)的代碼絕不會(huì)更改或破壞全局狀態(tài),有助于提高代碼的可測(cè)試性和可維護(hù)性
函數(shù)式編程采用聲明式的風(fēng)格,易于推理,提高代碼的可讀性。
函數(shù)式編程將函數(shù)視為積木,通過(guò)一等高階函數(shù)來(lái)提高代碼的模塊化和可重用性。
可以利用響應(yīng)式編程組合各個(gè)函數(shù)來(lái)降低事件驅(qū)動(dòng)程序的復(fù)雜性(這點(diǎn)后面可能會(huì)多帶帶拿一篇來(lái)進(jìn)行講解)。
文章內(nèi)容來(lái)至于《JavaScript函數(shù)式編程指南》
歡迎關(guān)注個(gè)人公眾號(hào)【前端桃園】
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/100166.html
摘要:函數(shù)式編程,一看這個(gè)詞,簡(jiǎn)直就是學(xué)院派的典范。所以這期周刊,我們就重點(diǎn)引入的函數(shù)式編程,淺入淺出,一窺函數(shù)式編程的思想,可能讓你對(duì)編程語(yǔ)言的理解更加融會(huì)貫通一些。但從根本上來(lái)說(shuō),函數(shù)式編程就是關(guān)于如使用通用的可復(fù)用函數(shù)進(jìn)行組合編程。 showImg(https://segmentfault.com/img/bVGQuc); 函數(shù)式編程(Functional Programming),一...
摘要:參考鏈接面向?qū)ο缶幊棠P同F(xiàn)在的很多編程語(yǔ)言基本都具有面向?qū)ο蟮乃枷?,比如等等,而面向?qū)ο蟮闹饕枷雽?duì)象,類(lèi),繼承,封裝,多態(tài)比較容易理解,這里就不多多描述了。 前言 在我們的日常日發(fā)和學(xué)習(xí)生活中會(huì)常常遇到一些名詞,比如 命令式編程模型,聲明式編程模型,xxx語(yǔ)言是面向?qū)ο蟮牡鹊?,這個(gè)編程模型到處可見(jiàn),但是始終搞不清是什么?什么語(yǔ)言又是什么編程模型,當(dāng)你新接觸一門(mén)語(yǔ)言的時(shí)候,有些問(wèn)題是需...
摘要:中的函數(shù)式編程思想匿名函數(shù)在函數(shù)式編程語(yǔ)言中,函數(shù)是可以沒(méi)有名字的,匿名函數(shù)通常表示可以完成某件事的一塊代碼。匿名函數(shù)中包含對(duì)的局部變量的引用,因此當(dāng)返回時(shí),的值被保留不會(huì)被垃圾回收機(jī)制回收,持續(xù)調(diào)用,將會(huì)改變的值。 1 函數(shù)式編程簡(jiǎn)介 函數(shù)式編程是和傳統(tǒng)命令式編程區(qū)分的一種編程思想,在函數(shù)式編程語(yǔ)言中,函數(shù)是第一類(lèi)的對(duì)象,也就是說(shuō),函數(shù) 不依賴于任何其他的對(duì)象而可以獨(dú)立存在,而在面向...
摘要:中的函數(shù)式編程思想匿名函數(shù)在函數(shù)式編程語(yǔ)言中,函數(shù)是可以沒(méi)有名字的,匿名函數(shù)通常表示可以完成某件事的一塊代碼。匿名函數(shù)中包含對(duì)的局部變量的引用,因此當(dāng)返回時(shí),的值被保留不會(huì)被垃圾回收機(jī)制回收,持續(xù)調(diào)用,將會(huì)改變的值。 1 函數(shù)式編程簡(jiǎn)介 函數(shù)式編程是和傳統(tǒng)命令式編程區(qū)分的一種編程思想,在函數(shù)式編程語(yǔ)言中,函數(shù)是第一類(lèi)的對(duì)象,也就是說(shuō),函數(shù) 不依賴于任何其他的對(duì)象而可以獨(dú)立存在,而在面向...
閱讀 2875·2021-11-23 09:51
閱讀 3617·2021-10-08 10:17
閱讀 1387·2021-10-08 10:05
閱讀 1431·2021-09-28 09:36
閱讀 1963·2021-09-13 10:30
閱讀 2304·2021-08-17 10:12
閱讀 1756·2019-08-30 15:54
閱讀 2085·2019-08-30 15:53