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