需先了解 JavaScript 函式編程,再進一步了解如何使用API 函式編程的高階函式
JavaScript 函式編程
要透過 JavaScript 程式語言來開發出函式編程的程式碼,意味著你所寫的 JavaScript 程式碼必須符合函式編程的一項或多項重要概念,其中最重要的應該就是「一等公民與高階函式 ( First-class and higher-order functions )」這項了。
在 JavaScript 程式語言中,函式 ( functions ) 包含兩個非常重要的觀念:
- 函式為一級物件 ( first-class object )
- 函式提供了變數的作用域 ( scope )
var add = function (a, b) { return a+b; } var calc = function (op, a, b) { return op(a, b) }; calc(add, 1, 2); // 3程式筆記:op 指的是要帶入的函式,add 是被代入函式,所以回傳結果 3
Higher-order function (高階函式)
要達成 Higher-order function 的必要條件,就是符合下列兩項任何一項以上條件,
就可以稱為「高階函式」:
- 可以將函式物件當成參數傳入另一個函式
- 可以將函式物件當成另一個函式的回傳值
var search = function (pattern) {
return function(str) {
return str.search(pattern);
};
}
var searchByWill = search(/Huang/); // => str.search(/Huang/)
searchByWill('Will Huang'); // 5
程式筆記:回傳str.search(/Huang/),因此輸入字串就會去找Huang的位置。請注意:函式就算定義為「高階函式」,也不一定就能稱為「函式編程」,符合函式編程有一定的要件,你還必須確保該函式要能「避免改變狀態」、「避免可變的資料」以及擁有「純函式」等特性。
ECMAScript 5.1 - 此篇文章會提到這三個API 函式編程的高階函式
Array.prototype.filter() - JavaScript | MDN
Array.prototype.map() - JavaScript | MDN
Array.prototype.reduce() - JavaScript |
JavaScript 規格中加入了幾個陣列的 API,這幾個 API 函式算是符合函式編程的高階函式
在開始說明這幾個高階函式前,我先新增一個陣列,當成之後範例程式碼的輸入資料:
var people = [ { "name": { "first": "Will", "last": "Huang" }, "company": "MINIASP" }, { "name": { "first": "James", "last": "Huang" }, "company": "Coolrare" }, { "name": { "first": "Jeff", "last": "Wu" }, "company": "MINIASP" } ]
Array.prototype.filter() - JavaScript | MDN
假設我們的需求是希望能透過程式找出 people 物件中 last name 為 'Huang' 的人,如果我們用傳統的程式風格來寫,程式碼可能會長這樣:
var i, person, filtered_people = [];
for(i=0; i<people.length; i++) {
person = people[i];
if(person.name.last === 'Huang') {
filtered_people.push(person);
}
}
console.log(filtered_people);
如果們改用函式編程的寫法,改用 Array.prototype.filter() 來過濾陣列,那麼程式碼會變成這樣:var lastNameIsHuang = function(person) { return person.name.last === 'Huang'; }; var filtered_people = people.filter(lastNameIsHuang); console.log(filtered_people);程式筆記:程式變得很好維護,過濾方法整個抽出來
Array.prototype.map() - JavaScript | MDN
這個陣列的 map() 函式也是一個高階函式,他與 filter() 不同的地方在於:
- filter() 函式會過濾原本陣列中的資料,並回傳一個全新的陣列。
- map() 函式則會轉換原本陣列中的每一個元素,並回傳一個全新的陣列。
var new_people = people.map(function(person) { return { name: person.name.first + ' ' + person.name.last, company: person.company }; }); console.log(new_people);
程式筆記:用起來跟C# 的Automapper很類似,可以依造需求改變排列
你可以發現 map() 函式會讀入每一個陣列元素,並且依序傳入 map() 的回呼函式中,每一次的回呼函式執行都只要回傳「新元素」即可,最後 map() 回傳的結果將會是一個全新的陣列,而且陣列中的每個元素也將會是全新的物件。
這邊的 map() 函式,我直接照著字面翻譯,就是一種「對應」功能,把一份完整的陣列「對應」到另一份全新的陣列,並且回傳這份全新、對應過的陣列。
Array.prototype.reduce() - JavaScript | MDN
假設我們的需求是希望能透過程式計算出每個元素的 company 屬性的總字元數,傳統的寫法一定是先宣告一個變數,然後跑個迴圈計算每個元素中的數值,不過函數編程的寫法就可以靠 reduce() 函式來幫我們完成這個連續計算作業。
var total_company_chars = people.reduce(function(sum, person) { return sum + person.company.length; }, 0); console.log(total_company_chars);
1.reduce() 函式會將 people 陣列中每個元素依序傳入回呼函式中執行
2.第一次執行回呼函數
第一個參數 sum 會傳入 reduce() 函式傳入的第 2 個參數 0
第二個參數 person 則會傳入陣列中的第 1 個元素
回呼函式的回傳值,預設會是第 2 次執行回呼函式的第一個參數
3.第二次執行回呼函數
第一個參數 sum 會傳入上一次執行回呼函數的回傳值
第二個參數 person 則會傳入陣列中的第 2 個元素
回呼函式的回傳值,預設會是第 3 次執行回呼函式的第一個參數
4.依此類推 … 直到傳入陣列中的最後一個元素
程式筆記:此函式用法建議先去看MDN,將公司名稱字元加總
- map() 函式則會將原本陣列中的每一個元素「對應」成另一個全新的陣列。
- reduce() 函式則會將陣列中的元素中每個元素「縮減」成一個結果,你也可以把 reduce() 想像成「彙整所有陣列元素,透過回呼函式的連續計算獲得一個縮減後的結果」。
剛剛講的這三個高階函式 filter()、map() 與 reduce() 是可以搭配使用的,如果正確使用這 3 個高階函數,並使用函式編程的方式來撰寫,我們的程式碼就會非常易讀、易懂、方便測試。
修改一下需求:
我想過濾出 last name 為 Huang 的陣列元素 Array.prototype.filter() - JavaScript | MDN
將篩選過後的陣列對應出全新的物件格式Array.prototype.map() - JavaScript | MDN
計算所有新陣列中每一個元素 name 屬性累計的字元數Array.prototype.reduce() - JavaScript | MDN
people .filter(function(person) { return person.name.last === 'Huang'; }) .map(function(person) { return { name: person.name.first + ' ' + person.name.last, company: person.company }; }) .reduce(function(sum, person) { return sum + person.company.length; }, 0);
你從上述程式可以看出,這段程式確實符合函式編程的幾個重要特性:
- 避免改變狀態
- 避免可變的資料
- 純函式
- 延遲評估 (Lazy evaluation)
函式語言並非萬能,別忘了函式編程 ( functional programming ) 只是一種程式設計方法,他用不同的思考方式來解決問題,在某些情境下,使用函式編程確實能帶來極大效益,但不代表他很適合用來解決所有問題。有的時候使用函式編程反而會犧牲許多程式的執行效率,這是拿非函式變成語言來寫函式編程的常見問題,用 JavaScript 來寫函數編程也會有相同的問題存在,而且通常改用函數編程後,執行效能會比跑一般迴圈慢個 3 ~ 5 倍之多 (比較程序編程與函數編程的效能差異),當你處理資料過大時,就比較會有機會遇到效能問題。不過撰寫網頁應用程式時,似乎不太容易發現有這麼大的效能差異,因為我們本來就不會在網頁中處理大量的資料來源,而且現今的電腦與瀏覽器在執行 JavaScript 的時候,真的還蠻快的!
最後,推薦一個由 ReactiveX 設計的 Functional Programming 教學網頁 ( Functional Programming in Javascript ),這個頁面總共有 41 個 Functional Programming 練習題,你要一關一關過才行,建議不要跳關,做到最後,我保證你一定可以完全理解如何在 JavaScript 使用 Functional Programming 開發程式!
相關連結
- unctional programming - Wikipedia
- 函數程式語言 - 維基百科,自由的百科全書
- 當 全世界的語言 都往 Functional Programming 發展 by Appletone | CodeData
- 函数式编程初探 - 阮一峰的网络日志
- Amazon.com: The Magical World of Functional Programming: Part I: Thinking Functional eBook: K Anand Kumar: Kindle Store
- Higher-order function - Wikipedia
- 高階函數 - 維基百科,自由的百科全書
- Higher-Order Functions in JavaScript
- Higher-Order Functions :: Eloquent JavaScript
- 從 JavaScript 的 Map/Reduce 談起 Functional Programming | Mozilla Tech | 謀智台客
- Functional Programming in JavaScript (SlideShare)
- 'functional programming javascript' on SlideShare
- 比較程序編程與函數編程的效能差異
- 線上電子書
- JSDC.tw
沒有留言:
張貼留言