用 JavaScript 介紹函數式編程
以 JavaScript 編程語言介紹函數式編程的主要概念。
函數式編程介紹
函數式編程(FP)是一種具有特定技術的編程範例。
在編程語言中,你會找到純函數式編程語言以及支持函數式編程技術的編程語言。
Haskell、Clojure 和 Scala 是一些最受歡迎的純函數式編程語言。
支持函數式編程技術的流行編程語言包括 JavaScript、Python、Ruby 等等。
函數式編程並不是一個新概念,實際上它的根源可以追溯到 1930 年代的 Lambda 演算,並影響了許多編程語言。
近年來,函數式編程一直在不斷發展,所以現在是學習它的完美時機。
在本課程中,我將使用 JavaScript 代碼示例介紹函數式編程的主要概念。
一級函數
在函數式編程語言中,函數是一級公民。
可以分配給變量
1 | const f = (m) => console.log(m) |
由於函數可以分配給變量,它們可以添加到對象中:
1 | const obj = { |
也可以添加到數組中:
1 | const a = [ |
可以用作其他函數的參數
1 | const f = (m) => () => console.log(m) |
可以由函數返回
1 | const createF = () => { |
高階函數
接受函數作為參數或返回函數的函數稱為高階函數。
在 JavaScript 標準庫中的例子包括 Array.map()
、 Array.filter()
和 Array.reduce()
,我們稍後會看到它們的用法。
聲明性編程
你可能聽說過「聲明性編程」這個詞。
讓我們將這個詞放入上下文中。
「聲明性」的對立面是 命令式。
一個命令式的方法是告訴機器(一般而言)它需要執行哪些步驟才能完成一項工作。
一種聲明性的方法是告訴機器你需要做什麼,然後讓它找到解決方案的細節。
當你有足夠的抽象層次時,你開始思考聲明性,停止思考低級構造,更多地從高級UI層次進行思考。
有人可能會認為C編程比彙編編程更具有聲明性,這是正確的。
HTML 是聲明性的,所以如果你從 1995 年開始使用 HTML,你實際上已經在構建聲明性的 UI20多年了。
JavaScript 可以以命令式或聲明性的編程方式進行編程。
例如,聲明性的編程方法是避免使用 循環,而是使用功能編程結構,如 map
、reduce
和 filter
,因為你的程序更抽象,更少關注告訴機器每個處理步驟的細節。
不變性
在函數式編程中,數據永遠不會改變。數據是不可變的。
變量永遠不會被更改。要更新它的值,你需要創建一個新的變量。
代替更改數組,要添加新項目,你需要創建一個新的數組,將舊數組與新項目連接起來。
在更改對象之前,將對象進行複製。
const
這就是為什麼 ES2015 中廣泛使用 const
的原因。ES2015 擁抱了函數式編程概念,const
可以強制變量的不可變性。
Object.assign()
ES2015 還提供了 Object.assign()
,它是創建對象的關鍵:
1 | const redObj = { color: 'red' } |
concat()
在 JavaScript 中,要將項目添加到數組中,我們通常使用數組上的 push()
方法,但該方法會改變原始數組,因此它不能用於函數式編程。
我們可以使用 concat()
方法:
1 | const a = [1, 2] |
或者我們可以使用展開運算符:
1 | const c = [...a, 3] |
filter()
從數組中刪除項目時,也是同樣的操作:不要使用 pop()
和 splice()
,它們會修改原始數組,可以使用 array.filter()
:
1 | const d = a.filter((v, k) => k < 1) |
純度
純函數:
- 永遠不會更改通過引用傳遞給它的任何參數(在 JS 中,對象和數組):它們應被視為不可變。當然,它可以更改按值複製的任何參數
- 純函數的返回值不受其它任何東西的影響,只受其輸入參數的影響:傳遞相同的參數始終返回相同的輸出
- 在執行過程中,純函數不會更改其外部的任何東西
數據轉換
由於不可變性是函數式編程的重要概念和基礎,你可能會問數據如何更改。
簡單:通過創建副本來更改數據。
特別是,函數通過返回數據的新副本來更改數據。
執行此操作的核心函數是 map 和 reduce。
Array.map()
在數組上調用 Array.map()
會在原始數組的每個項目上執行一個函數,並創建一個新數組,其值為該函數的結果:
1 | const a = [1, 2, 3] |
Array.reduce()
在數組上調用 Array.reduce()
可以將該數組轉換為其他任意類型,包括純量、函數、布爾值和對象。
你需要傳遞一個處理結果的函數和一個起始點:
1 | const a = [1, 2, 3] |
1 | const o = a.reduce((obj, k) => { obj[k] = k; return obj }, {}) |
遞歸
遞歸是函數式編程的一個重點。當一個函數調用自身時,稱為 遞歸函數。
遞歸的典型例子是費波那契數列(N = (N-1 + N-2))計算,在這裡我們使用完全低效(但好讀)的解決方案:
1 | var f = (n) => n <= 1 ? 1 : f(n-1) + f(n-2) |
組合
組合是函數式編程的另一個重要主題,這正是將它放入“關鍵主題”列表的好原因。
組合是通過結合較簡單的函數而生成高階函數的過程。
在純 JS 中進行組合
在純 JavaScript 中,組合函數的一種常見方法是將它們鏈接在一起:
1 | obj.doSomething() |
或者,也非常常用的是通過將函數執行傳遞到函數中:
1 | obj.doSomething(doThis()) |
使用 lodash
進行組合
更一般地,組合是將許多函數打包在一起執行更複雜操作的過程。
lodash/fp
提供了 compose
的實現:我們執行一個函數列表,從一個參數開始,每個函數從前一個函數的返回值中繼承參數。請注意,我們不需要在任何地方存儲中間值。
import { compose } from 'lodash/fp'
const slugify = compose(
encodeURIComponent,
join('-'),
map(toLowerCase),
split(' ')
)
slufigy('Hello World') // hello-world