天天看點

JavaScript函數式程式設計思想

歡迎通路個人網站

最開始接觸函數式程式設計的時候是在小米工作的時候,那個時候看老大以前寫的代碼各種 compose,然後一些 ramda 的一些工具函數,看着很吃力,然後極力吐槽函數式程式設計,現在回想起來,那個時候的自己真的是見識短淺,隻想說,'真香'。

最近在研究函數式程式設計,真的是在學習的過程中感覺自己的思維提升了很多,抽象能力大大的提高了,讓我深深的感受到了函數式程式設計的魅力。是以我打算後面用 5 到 8 篇的篇幅,詳細的介紹一下函數式程式設計的思想,基礎、如何設計、測試等。

今天這篇文章主要介紹函數式程式設計的思想。

  • 函數式程式設計有用嗎?
  • 什麼是函數式程式設計?
  • 函數式程式設計的優點。

面向對象程式設計(OOP)通過封裝變化使得代碼更易了解。

函數式程式設計(FP)通過最小化變化使得代碼更易了解。

-- Michacel Feathers(Twitter)

總所周知 JavaScript 是一種擁有很多共享狀态的動态語言,慢慢的,代碼就會積累足夠的複雜性,變得笨拙難以維護。面向對象設計能幫我們在一定程度上解決這個問題,但是還不夠。

由于有很多的狀态,是以處理資料流和變化的傳遞顯得尤為重要,不知道你們知道響應式程式設計與否,這種程式設計範式有助于處理 JavaScript 的異步或者事件響應。總之,當我們在設計應用程式的時候,我們應該考慮是否遵守了以下的設計原則。

  • 可擴充性--我是否需要不斷地重構代碼來支援額外的功能?
  • 易子產品化--如果我更改了一個檔案,另一個檔案是否會受到影響?
  • 可重用性--是否有很多重複的代碼?
  • 可測性--給這些函數添加單元測試是否讓我糾結?
  • 易推理性--我寫的代碼是否非結構化嚴重并難以推理?

我這能這麼跟你說,一旦你學會了函數式程式設計,這些問題迎刃而解,本來函數式程式設計就是這個思想,一旦你掌握了函數式,然後你再學習響應式程式設計那就比較容易懂了,這是我親身體會的。我之前在學 Rxjs 的時候是真的痛苦,說實話,Rxjs 是我學過最難的庫了,沒有之一。在經曆過痛苦的一兩個月之後,有些東西還是不能融會貫通,知道我最近研究函數式程式設計,才覺得是理所當然。毫無誇張,我也盡量在後面的文章中給大家介紹一下 Rxjs,這個話題我也在公司分享過。

什麼是函數式程式設計?

簡單來說,函數式程式設計是一種強調以函數使用為主的軟體開發風格。看到這句我想你還是一臉懵逼,不知道函數式程式設計是啥,不要着急,看到最後我相信你會明白的。

還有一點你要記住,函數式程式設計的目的是使用函數來抽象作用在資料之上的控制流和操作,進而在系統中消除副作用并減少對狀态的改變。

下面我們通過例子來簡單的示範一下函數式程式設計的魅力。

現在的需求就是輸出在網頁上輸出

“Hello World”

可能初學者會這麼寫。

document.querySelector('#msg').innerHTML = '<h1>Hello World</h1>'
           

這個程式很簡單,但是所有代碼都是死的,不能重用,如果想改變消息的格式、内容等就需要重寫整個表達式,是以可能有經驗的前端開發者會這麼寫。

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 a - b
    })
}

var arr = [1, 3, 2]
sortDesc(arr) // [1, 2, 3]
arr // [1, 2, 3]
           

這段代碼看似沒什麼問題,但是會導緻在排序的過程中會産生副作用,修改了原始引用,可以看到原始的 arr 變成了

[1, 2, 3]

。這是一個語言缺陷,後面會介紹如何克服。

總結

  • 使用純函數的代碼絕不會更改或破壞全局狀态,有助于提高代碼的可測試性和可維護性
  • 函數式程式設計采用聲明式的風格,易于推理,提高代碼的可讀性。
  • 函數式程式設計将函數視為積木,通過一等高階函數來提高代碼的子產品化和可重用性。
  • 可以利用響應式程式設計組合各個函數來降低事件驅動程式的複雜性(這點後面可能會單獨拿一篇來進行講解)。
内容來至于《JavaScript函數式程式設計指南》

歡迎關注個人公衆号【前端桃園】。

JavaScript函數式程式設計思想
下一篇: 2333333333

繼續閱讀