React躬行記(1)——函數語言程式設計
函數語言程式設計是React的精髓,在正式講解React之前,有必要先了解一下函數語言程式設計,有助於更好的理解React的特點。函數語言程式設計(Functional Programming)不是一種新的框架或工具,而是一種以函式為主的程式設計正規化。程式設計正規化也叫程式設計範型,是一類程式設計風格,除了函數語言程式設計,常用的還有面向物件程式設計、指令式程式設計等。
一、宣告式程式設計
宣告時程式設計也是一種正規化,但它是一個比較大的概念,函數語言程式設計是它的一個子集。宣告式程式設計能指定每一步操作,而不用向計算機描述具體的實現細節。與之相對立的是指令式程式設計,它會命令計算機每一步該怎麼做。以陣列的元素翻倍為例,先用指令式程式設計實現,如下所示。
var arr = [1, 2, 3], length = arr.length, doubles = []; for (let i = 0; i < length; i++) { doubles.push(arr[i] * 2); }
在命令式的程式碼中,先用for迴圈遍歷整個陣列,然後讓每個元素乘以二,再將計算結果插入到doubles陣列中,直至將所有的元素計算完才終止整套操作。改用宣告式程式設計可以像下面這樣實現相同的功能。
var doubles = [1, 2, 3].map(value => value * 2);
在宣告式的程式碼中,用map()方法替代了迴圈語句(即不指明流程的控制方式),既不用再維護計數器,也不用再通過索引訪問陣列的元素,配合ES6的箭頭函式讓整套操作變得非常簡潔。
除了這些表面區別之外,還有個最本質的區別,那就是宣告式程式設計會避免用變數儲存程式的狀態,從而能提高程式碼的無狀態性。在命令式的程式碼中,每次迭代都會修改doubles變數,這是個狀態變數,而在宣告式的程式碼中,改用返回值儲存程式的狀態。
二、函式優先
函數語言程式設計強調在程式中使用函式。由於JavaScript中的函式是一等公民,它既可以是變數的值,也可以作為另一函式的引數或返回值,因此通過函式可構建一層抽象以替代流程控制或解決複雜的邏輯操作。例如對陣列中的數字進行排序和過濾,可以像下面這樣運用函數語言程式設計的思想實現。
[4, 1, 5, 2, 3].sort((a, b) => a > b).filter(value => value > 2); //[3, 4, 5]
函數語言程式設計旨在將複雜的運算分解成一系列巢狀的函式,逐層推導,不斷漸進,直至完成運算。
三、純函式
純函式(Pure Function)是一種沒有副作用、引用透明的函式,它是函數語言程式設計的基本概念,接下來會重點講解它的三個特徵。
1)函式的副作用
函式在讀寫外部資源或執行不確定的操作時就會產生副作用,例如修改函式外的變數、呼叫Date.now()或Math.random()、更新cookie資訊等。副作用不僅會降低程式整體的可讀性,有時候還會帶來意料之外、難以排查的錯誤,下面是一個副作用的例子。
var digit = 1; function increment() { digit += Math.random(); return digit; }
在上面的程式碼中,increment()函式產生了副作用,因為每次呼叫它都會更新外部的digit變數,並且每次得到的計算結果也無法預知。
2)引用透明
如果傳遞給函式相同的引數,始終能得到相同的結果,那麼就能說這個函式是引用透明(Referential Transparent)的。簡單的說就是,函式的執行只受其輸入值的影響,如下程式碼所示,傳遞給add()函式固定的引數會返回固定的值。
function add(a, b) { return a + b; }
3)引數值不可變
傳遞給純函式的引數值是不允許在內部將其改變的,換句話說,在函式內部使用的是引數值的副本。如果引數值是基本型別的,那麼傳遞給函式的就是其副本;但如果引數值是物件型別的,那麼需要注意,傳遞給函式的是引用物件的指標。
下面用一個示例說明,addDigit()函式的引數是一個數組,它的功能是為陣列的每個元素加一,在執行addDigit(digits)之後,由於digits變數是一個數組,因此它的元素會隨著函式的呼叫而被改變。
var digits = [1, 2, 3]; function addDigit(arr) { for (let i = 0, len = arr.length; i < len; i++) { arr[i] += 1; } return arr; } addDigit(digits); console.log(digits); //[2, 3, 4]
接下來修改addDigit()函式,使之能滿足純函式的要求,如下所示。
var digits = [1, 2, 3]; function addDigit(arr) { return arr.map(value => value + 1); } addDigit(digits); console.log(digits); //[1, 2, 3]
在addDigit()函式內部,用map()方法替代for迴圈,使得在不改變引數的前提下,完成元素加一的功能。
四、優點
函數語言程式設計有許多優點,本節只列出了其中的兩點。
(1)函數語言程式設計可將複雜的任務分解成一個個既簡單又獨立的純函式,有利於提高程式碼的模組化、複用性、預測性以及可測試性。
(2)函數語言程式設計有很高的自由度,可以採用更符合人類思維習慣的鏈式寫法,以此提高程式碼的可讀性。
接下來會用兩種函式式的寫法操作一個數組,為了便於演示省略了函式的具體實現,首先是普通的函式式寫法,如下所示。
elementDouble(filterEven(arr, filterFn), doubleFn);
兩個函式都有兩個引數,第一個是陣列,第二個是相應的回撥函式。具體的執行過程是先通過filterEven()函式過濾掉陣列中偶數位置的元素,再用elementDouble()函式把每個元素翻倍,下面改成鏈式的寫法。
filerEven(arr, filterFn).elementDouble(arr, doubleFn);
通過兩段程式碼的對比可以看出,鏈式的寫法更容易讓人理解,程式碼意圖也更清晰。
&n