RxJS 系列目錄
- RxJS 系列之一 - Functional Programming 簡介 (本文)
- RxJS 系列之二 - Observable 詳解
- RxJS 系列之三 - Operators 詳解
- RxJS 系列之四 - Subject 詳解
- RxJS 系列之五 - RxJS 實戰 (未完成)
- RxJS 系列之六 - RxJS 在 Angular 2 中的應用 (未完成)
什麼是函數語言程式設計
簡單說,"函數語言程式設計"是一種 "程式設計正規化"(programming paradigm),也就是如何編寫程式的方法論。
它屬於 "結構化程式設計" 的一種,主要思想是把運算過程儘量寫成一系列巢狀的函式呼叫。舉例來說,現在有這樣一個數學表示式:
(5+6) - 1 * 3複製程式碼
傳統的程式式程式設計,可能這樣寫:
var a = 5 + 6;
var b = 1 * 3;
var c = a - b;複製程式碼
函數語言程式設計要求使用函式,我們可以把運算定義成不同的函式:
const add = (a, b) => a + b;
const mul = (a, b) => a * b;
const sub = (a,b) => a - b;
sub(add(5,6), mul(1,3));複製程式碼
我們把每個運算包成一個個不同的函式,並且根據這些函式組合出我們要的結果,這就是最簡單的函數語言程式設計。
函數語言程式設計基礎條件
函式為一等公民 (First Class)
所謂 "一等公民"(first class),指的是函式與其他資料型別一樣,處於平等地位,可以賦值給其他變數,也可以作為引數,傳入另一個函式,或者作為其它函式的返回值。
函式賦值給變數:
const greet = function(msg) { console.log(`Hello ${msg}`); }
greet('Semlinker'); // Output: 'Hello Semlinker'複製程式碼
函式作為引數:
const logger = function(msg) { console.log(`Hello ${msg}`); };
const greet = function(msg, print) { print(msg); };
greet('Semlinker', logger);複製程式碼
函式作為返回值:
const a = function(a) {
return function(b) {
return a + b;
};
};
const add5 = a(5);
add5(10); // Output: 15複製程式碼
函數語言程式設計重要特性
只用表示式,不用語句
"表示式"(expression)是一個單純的運算過程,總是有返回值;"語句"(statement)是執行某種操作,沒有返回值。函數語言程式設計要求,只使用表示式,不使用語句。也就是說,每一步都是單純的運算,而且都有返回值。
原因是函數語言程式設計的開發動機,一開始就是為了處理運算(computation),不考慮系統的讀寫(I/O)。"語句"屬於對系統的讀寫操作,所以就被排斥在外。
Pure Function
Pure Function (純函式) 的特點:
- 給定相同的輸入引數,總是返回相同的結果
- 沒有產生任何副作用
- 沒有依賴外部變數的值
所謂 "副作用")(side effect),是指函式內做了與本身運算無關的事,比如修改某個全域性變數的值,或傳送 HTTP 請求,甚至函式體內執行 console.log
都算是副作用。函數語言程式設計強調函式不能有副作用,也就是函式要保持純粹,只執行相關運算並返回值,沒有其他額外的行為。
前端中常見的產生副作用的場景:
- 傳送 HTTP 請求
- 函式內呼叫 logger 函式,如 console.log、console.dir 等
- 修改外部變數的值
- 函式內執行 DOM 操作
接下來我們看一下純函式與非純函式的具體示例:
純函式示例:
const double = (number) => number * 2;
double(5);複製程式碼
非純函式示例:
Math.random(); // => 0.3384159509502669
Math.random(); // => 0.9498302571942787
Math.random(); // => 0.9860841663478281複製程式碼
不修改狀態 - 利用引數儲存狀態
函數語言程式設計只是返回新的值,不修改系統變數。因此,不修改變數,也是它的一個重要特點。
在其他型別的語言中,變數往往用來儲存"狀態"(state)。不修改變數,意味著狀態不能儲存在變數中。函數語言程式設計使用引數儲存狀態,最好的例子就是遞迴,具體示例如下:
function findIndex(arr, predicate, start = 0) {
if (0 <= start && start < arr.length) {
if (predicate(arr[start])) {
return start;
}
return findIndex(arr, predicate, start+1);
}
}
findIndex(['a', 'b'], x => x === 'b'); // 查詢陣列中'b'的索引值複製程式碼
示例中的 findIndex 函式用於查詢陣列中某個元素的索引值,我們通過 start 引數來儲存當前的索引值,這就是利用引數儲存狀態。
引用透明
引用透明(Referential transparency),指的是函式的執行不依賴於外部變數或 "狀態",只依賴於輸入的引數,任何時候只要引數相同,引用函式所得到的返回值總是相同的。
非引用透明的示例:
const FIVE = 5;
const addFive = (num) => num + FIVE;
addFive(10);複製程式碼
函數語言程式設計的優勢
1.程式碼簡潔,開發快速
函數語言程式設計大量使用函式,減少了程式碼的重複,因此程式比較短,開發速度較快。
2.接近自然語言,易於理解,可讀性高
函數語言程式設計的自由度很高,可以寫出很接近自然語言的程式碼。我們可以通過一系列的函式,封裝資料的處理過程,程式碼會變得非常簡潔且可讀性高,具體參考以下示例:
[1,2,3,4,5].map(x => x * 2).filter(x => x > 5).reduce((p,n) => p + n);複製程式碼
3.可維護性高、方便程式碼管理
函數語言程式設計不依賴、也不會改變外界的狀態,只要給定輸入引數,返回的結果必定相同。因此,每一個函式都可以被看做獨立單元,很有利於進行單元測試(unit testing)和除錯(debugging),以及模組化組合。
4.易於"併發程式設計"
函數語言程式設計不需要考慮"死鎖"(deadlock),因為它不修改變數,所以根本不存在"鎖"執行緒的問題。不必擔心一個執行緒的資料,被另一個執行緒修改,所以可以很放心地把工作分攤到多個執行緒,部署"併發程式設計"(concurrency)。
函數語言程式設計中常用方法
forEach
在 ES 5 版本之前,我們只能通過 for 迴圈遍歷陣列:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
for (var i =0, len = heroes.length; i < len; i++) {
console.log(heroes[i]);
}複製程式碼
在 ES 5 版本之後,我們可以使用 forEach 方法,實現上面的功能:
forEach 方法簽名:
array.forEach(callback[, thisArg])複製程式碼
引數說明:
- callback - 對陣列中每一項,進行處理的函式
- currentValue - 陣列中正在處理的當前元素
- index - 陣列中正在處理的當前元素的索引
- array - 處理的陣列
- thisArg (可選的) - 設定執行 callback 函式時,this 的值
以上示例 forEach 方法實現:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
heroes.forEach(name => console.log(name));複製程式碼
map
在 ES 5 版本之前,對於上面的示例,如果我們想給每個英雄的名字新增一個字首,但不改變原來的陣列,我們可以這樣實現:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
var prefixedHeroes = [];
for (var i =0, len = heroes.length; i < len; i++) {
prefixedHeroes.push('Super_' + heroes[i]);
}複製程式碼
在 ES 5 版本之後,我們可以使用 map 方法,方便地實現上面的功能。
map 方法簽名:
const new_array = arr.map(callback[, thisArg])複製程式碼
引數說明:
- callback - 對陣列中每一項,進行對映處理的函式
- currentValue - 陣列中正在處理的當前元素
- index - 陣列中正在處理的當前元素的索引
- array - 處理的陣列
- thisArg (可選的) - 設定執行 callback 函式時,this 的值
以上示例 map 方法實現:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
var prefixedHeroes = heroes.map(name => 'Super_' + name);複製程式碼
filter
在 ES 5 版本之前,對於 heroes 陣列,我們想獲取名字中包含 m
字母的英雄,我們可以這樣實現:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
var filterHeroes = [];
for (var i =0, len = heroes.length; i < len; i++) {
if(/m/i.test(heroes[i])) {
filterHeroes.push(heroes[i]);
}
}複製程式碼
在 ES 5 版本之後,我們可以使用 filter 方法,方便地實現上面的功能。
filter 方法簽名:
var new_array = arr.filter(callback[, thisArg])複製程式碼
引數說明:
- callback - 用來測試陣列的每個元素的函式。呼叫時使用引數 (element, index, array)。返回true表示保留該元素(通過測試),false則不保留。
- thisArg (可選的) - 設定執行 callback 函式時,this 的值
以上示例 filter 方法實現:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
var filterRe = /m/i;
var filterHeroes = heroes.filter(name => filterRe.test(name));複製程式碼