層級: 初級
Shantanu Bhattacharya (shantanu@justawordaway.com), 首席顧問, Siemens Information Systems Limited
2006 年 7 月 20 日
函數式或宣告式程式設計是非常強大的編程方法,正逐漸在軟體行業流行起來。這篇文章將介紹一些相關的函數式編程概念,並提供有效使用這些概念的樣本。作者將解釋如何使用 JavaScript(TM)(JavaScript 能匯入函數式編程的構造和特性)編寫優美的代碼。
簡介
函數式程式設計語言在學術領域已經存在相當長一段時間了,但是從曆史上看,它們沒有豐富的工具和庫可供使用。隨著 .NET 平台上的 Haskell 的出現,函數式編程變得更加流行。一些傳統的程式設計語言,例如 C++ 和 JavaScript,引入了由函數式編程提供的一些構造和特性。在許多情況下,JavaScript 的重複代碼導致了一些拙劣的編碼。如果使用函數式編程,就可以避免這些問題。此外,可以利用函數式編程風格編寫更加優美的回調。
|
函數式編程 函數式編程只描述在程式輸入上執行的操作,不必使用臨時變數儲存中間結果。重點是捕捉 “是什麼以及為什麼”,而不是 “如何做”。與將重點放在執行連續命令上的過程性編程相比,函數式編程的重點是函數的定義而不是狀態機器(state machine)的實現。 大型知識管理系統應用程式從使用函數式編程風格上受益頗多,因為函數式編程簡化了開發。 |
|
因為函數式編程採用了完全不同的組織程式的方式,所以那些習慣於採用命令式範例的程式員可能會發現函數式編程有點難學。在這篇文章中,您將瞭解一些關於如何採用函數式風格,用 JavaScript 編寫良好的、優美的代碼的樣本。我將討論:
- 函數式編程概念,包括匿名函數、調用函數的不同方法,以及將函數作為參數傳遞給其他函數的方式。
- 函數式概念的運用,採用的樣本包括:擴充數組排序;動態超文字標記語言 產生的優美代碼;系列函數的應用。
函數式編程概念
在那些通過描述 “如何做” 指定解決問題的方法的語言中,許多開發人員都知道如何進行編碼。例如,要編寫一個計算階乘的函數,我可以編寫一個迴圈來描述程式,或者使用遞迴來尋找所有數位乘積。在這兩種情況下,計算的過程都在程式中進行了詳細說明。清單 1 顯示了一個計算階乘的可能使用的 C 代碼。
清單 1. 過程風格的階乘
int factorial (int n){ if (n <= 0) return 1; else return n * factorial (n-1);} |
這類語言也叫做過程性 程式設計語言,因為它們定義瞭解決問題的過程。函數式編程與這個原理有顯著不同。在函數式編程中,需要描述問題 “是什麼”。 函數式程式設計語言又叫做聲明性 語言。同樣的計算階乘的程式可以寫成所有到 n 的數位乘積。計算階乘的典型函數式程式看起來如 清單 2 中的樣本所示。
清單 2. 函數式風格的階乘
factorial n, where n <= 0 := 1factorial n := foldr * 1 take n [1..] |
第二個語句指明要得到從 1 開始的前 n 個數位列表(take n [1..]
),然後找出它們的乘積,1 為基元。這個定義與前面的樣本不同,沒有迴圈或遞迴。它就像階乘函數的算術定義。一旦瞭解了庫函數(take
和 foldr
)和標記(list notation [ ]
)的意義,編寫代碼就很容易,而且可讀性也很好。
|
只用三行 Miranda 代碼就可以編寫常式,根據參數,使用廣度優先或深度優先遍曆處理 n 叉樹的每個節點,而且元素可以是任何通用類型。 |
|
從曆史上看,函數式程式設計語言不太流行有各種原因。但是最近,有些函數式程式設計語言正在進入電腦行業。其中一個例子就是 .NET 平台上的 Haskell。其他情況下,現有的一些語言借用了函數式程式設計語言中的一些概念。一些 C++ 實現中的迭代器和 continuation,以及 JavaScript 中提供的一些函數式構造(functional construct),就是這種借用的樣本。但是,通過借用函數式構造,總的語言編程範例並沒有發生變化。JavaScript 並沒因為函數式構造的添加就變成了函數式程式設計語言。
我現在要討論 JavaScript 中的函數式構造的各種美妙之處,以及在日常編碼和工作中使用它們的方式。我們將從一些準系統開始,然後用它們查看一些更有趣的應用。
匿名函數
在 JavaScript 中,可以編寫匿名函數或沒有名稱的函數。為什麼需要這樣的函數?請繼續往下讀,但首先我們將學習如何編寫這樣一個函數。如果擁有以下 JavaScript 函數:
清單 3. 典型的函數
function sum(x,y,z) { return (x+y+z);} |
然後對應的匿名函數看起來應當如下所示:
清單 4. 匿名函數
function(x,y,z) { return (x+y+z);} |
要使用它,則需要編寫以下代碼:
清單 5. 應用匿名函數
var sum = function(x,y,z) { return (x+y+z);}(1,2,3);alert(sum); |
使用函數作為值
也可以將函數作為值使用。還可以擁有一些所賦值是函數的變數。在最後一個樣本中,還可以執行以下操作:
清單 6. 使用函數賦值
var sum = function(x,y,z) { return (x+y+z);}alert(sum(1,2,3)); |
在上面 清單 6 的樣本中,為變數 sum 賦的值是函數定義本身。這樣,sum 就成了一個函數,可以在任何地方調用。
調用函數的不同方法
JavaScript 允許用兩種方式調用函數,如清單 7 和 8 所示。
清單 7. 典型的函數應用
或
清單 8. 用函數作為運算式
(alert) (“Hello, World!"); |
所以也可以編寫以下代碼:
清單 9. 定義函數之後就可以立即使用它
( function(x,y,z) { return (x+y+z) } ) (1, 2, 3); |
可以在括弧中編寫函數運算式,然後傳遞給參數,對參數進行運算。雖然在 清單 8 的樣本中,有直接包含在括弧中的函數名稱,但是按 清單 9 中所示方式使用它時,就不是這樣了。
將函數作為參數傳遞給其他函數
也可以將函數作為參數傳遞給其他函數。雖然這不是什麼新概念,但是在後續的樣本中大量的使用了這個概念。可以傳遞函數參數,如 清單 10 所示。
清單 10. 將函數作為參數傳遞,並應用該函數
var passFunAndApply = function (fn,x,y,z) { return fn(x,y,z); };var sum = function(x,y,z) { return x+y+z;};alert( passFunAndApply(sum,3,4,5) ); // 12 |
執行最後一個 alert 語句輸出了一個大小為 12 的值。
使用函數式概念
前一節介紹了一些使用函數式風格的編程概念。所給的樣本並沒有包含所有的概念,它們在重要性方面也沒有先後順序,只是一些與這個討論有關的概念而已。下面對 JavaScript 中的函數式風格作一快速總結:
- 函數並不總是需要名稱。
- 函數可以像其他值一樣分配給變數。
- 函數運算式可以編寫並放在括弧中,留待以後應用。
- 函數可以作為參數傳遞給其他函數。
這一節將介紹一些有效使用這些概念編寫優美的 JavaScript 代碼的樣本。(使用 JavaScript 函數式風格,可以做許多超出這個討論範圍的事。)
-
擴充數組排序
-
先來編寫一個排序方法,可以根據數組元素的日期對資料進行排序。用 JavaScript 編寫這個方法非常簡單。資料對象的排序方法接受一個選擇性參數,這個選擇性參數就是比較函數。在這裡,需要使用 清單 11 中的比較函數。
清單 11. 比較函數
function (x,y) {return x.date – y.date;} |
要得到需要的函數,請使用 清單 12 的樣本。
清單 12. 排序函數的擴充
arr.sort( function (x,y) {return x.date – y.date; } ); |
其中 arr 是類型數組對象。排序函數會根據 arr 數組中對象的日期對所有對象進行排序。比較函數和它的定義一起被傳遞給排序函數,以完成排序操作。使用這個函數:
- 每個 JavaScript 對象都有一個 date 屬性。
- JavaScript 的數群組類型的排序函數接受選擇性參數,選擇性參數是用來排序的比較函數。這與 C 庫中的
qsort
函數類似。
-
動態產生 HTML 的優美代碼
-
在這個樣本中,將看到如何編寫優美的代碼,從數組動態地產生 HTML。可以根據從資料中得到的值產生表格。或者,也可以用數組的內容產生排序和未排序的列表。也可以產生垂直或水平的功能表項目。
清單 13 中的代碼風格通常被用來從數組產生動態超文字標記語言。
清單 13. 產生動態超文字標記語言 的普通代碼
var str=' ';for (var i=0;i<arr.length;i++) { var element=arr[i]; str+=... HTML generation code...}document.write(str); |
可以用 清單 14 的代碼替換這個代碼。
清單 14. 產生動態超文字標記語言 的通用方式
Array.prototype.fold=function(templateFn) { var len=this.length; var str=' '; for (var i=0 ; i<len ; i++) str+=templateFn(this[i]); return str;}function templateInstance(element) { return ... HTML generation code ...}document.write(arr.fold(templateInstance)); |
我使用 Array
類型的 prototype 屬性定義新函數 fold。現在可以在後面定義的任何數組中使用該函數。
-
系列函數的應用
-
考慮以下這種情況:想用一組函數作為回呼函數。為實現這一目的,將使用
window.setTimeout
函數,該函數有兩個參數。第一個參數是在第二個參數表示的毫秒數之後被調用的函數。清單 15 顯示了完成此操作的一種方法。
清單 15. 在回調中調用一組函數
window.setTimeout(function(){alert(‘First!');alert(‘Second!');}, 5000); |
清單 16 顯示了完成此操作的更好的方式。
清單 16. 調用系列函數的更好的方式
Function.prototype.sequence=function(g) { var f=this; return function() { f();g(); }};function alertFrst() { alert(‘First!'); }function alertSec() { alert(‘Second!'); }setTimeout( alertFrst.sequence(alertSec), 5000); |
在處理事件時,如果想在調用完一個回調之後再調用一個回調,也可以使用 清單 16 中的代碼擴充。這可能是一個需要您自行完成的一個練習,現在您的興趣被點燃了吧。