要說 JavaScript 和其他較為常用的語言最大的不同是什么,那無疑就是 JavaScript 是函數(shù)式的語言,函數(shù)式語言的特點如下:
函數(shù)為第一等的元素,即人們常說的一等公民。就是說,在函數(shù)式編程中,函數(shù)是不依賴于其他對象而獨立存在的(對比與Java,函數(shù)必須依賴對象,方法是對象的方法)。
函數(shù)可以保持自己內(nèi)部的數(shù)據(jù),函數(shù)的運算對外部無副作用(修改了外部的全局變量的狀態(tài)等),關(guān)于函數(shù)可以保持自己內(nèi)部的數(shù)據(jù)這一特性,稱之為閉包。我們可以來看一個簡單的例子:
var outter = function(){
var x = 0;
return function(){
return x++;
}
}
var a = outter();
print(a());
print(a());
var b = outter();
print(b());
print(b());
運行結(jié)果為:
0
1
0
1
變量 a 通過閉包引用 outter 的一個內(nèi)部變量,每次調(diào)用 a()就會改變此內(nèi)部變量,應(yīng)該注意的是,當調(diào)用a時,函數(shù) outter 已經(jīng)返回了,但是內(nèi)部變量x的值仍然被保持。而變量b也引用了 outter,但是是一個不同的閉包,所以 b 開始引用的 x 值不會隨著 a()被調(diào)用而改變,兩者有不同的實例,這就相當于面向?qū)ο笾械牟煌瑢嵗龘碛胁煌乃接袑傩?,互不干涉?/p>
由于 JavaScript 支持函數(shù)式編程,我們隨后會發(fā)現(xiàn) JavaScript 許多優(yōu)美而強大的能力,這些能力得力于以下主題:匿名函數(shù),高階函數(shù),閉包及柯里化等。熟悉命令式語言的開發(fā)人員可能對此感到陌生,但是使用 lisp, scheme 等函數(shù)式語言的開發(fā)人員則覺得非常親切。
匿名函數(shù)在函數(shù)式編程語言中,術(shù)語成為 lambda 表達式。顧名思義,匿名函數(shù)就是沒有名字的函數(shù),這個是與日常開發(fā)中使用的語言有很大不同的,比如在 C/Java 中,函數(shù)和方法必須有名字才可以被調(diào)用。在 JavaScript 中,函數(shù)可以沒有名字,而且這一個特點有著非凡的意義:
function func(){
//do something
}
var func = function(){
//do something
}
這兩個語句的意義是一樣的,它們都表示,為全局對象添加一個屬性func,屬性func的值為一個函數(shù)對象,而這個函數(shù)對象是匿名的。匿名函數(shù)的用途非常廣泛,在JavaScript代碼中,我們經(jīng)??梢钥吹竭@樣的代碼:
var mapped = [1, 2, 3, 4, 5].map(function(x){return x * 2});
print(mapped);
應(yīng)該注意的是,map這個函數(shù)的參數(shù)是一個匿名函數(shù),你不需要顯式的聲明一個函數(shù),然后將其作為參數(shù)傳入,你只需要臨時聲明一個匿名的函數(shù),這個函數(shù)被使用之后就別釋放了。在高階函數(shù)這一節(jié)中更可以看到這一點。
通常,以一個或多個函數(shù)為參數(shù)的函數(shù)稱之為高階函數(shù)。高階函數(shù)在命令式編程語言中有對應(yīng)的實現(xiàn),比如C語言中的函數(shù)指針,Java 中的匿名類等,但是這些實現(xiàn)相對于命令式編程語言的其他概念,顯得更為復雜。
Lisp中,對列表有一個map操作,map接受一個函數(shù)作為參數(shù),map對列表中的所有元素應(yīng)用該函數(shù),最后返回處理后的列表(有的實現(xiàn)則會修改原列表),我們在這一小節(jié)中分別用JavaScript/C/Java來對map操作進行實現(xiàn),并對這些實現(xiàn)方式進行對比:
Array.prototype.map = function(func /*, obj */){
var len = this.length;
//check the argument
if(typeof func != "function"){
throw new Error("argument should be a function!");
}
var res = [];
var obj = arguments[1];
for(var i = 0; i < len; i++){
//func.call(), apply the func to this[i]
res[i] = func.call(obj, this[i], i, this);
}
return res;
}
我們對 JavaScript 的原生對象 Array 的原型進行擴展,函數(shù) map 接受一個函數(shù)作為參數(shù),然后對數(shù)組的每一個元素都應(yīng)用該函數(shù),最后返回一個新的數(shù)組,而不影響原數(shù)組。由于 map 函數(shù)接受的是一個函數(shù)作為參數(shù),因此 map 是一個高階函數(shù)。我們進行測試如下:
function double(x){
return x * 2;
}
[1, 2, 3, 4, 5].map(double);//return [2, 4, 6, 8, 10]
應(yīng)該注意的是 double 是一個函數(shù)。根據(jù)上一節(jié)中提到的匿名函數(shù),我們可以為 map 傳遞一個匿名函數(shù):
var mapped = [1, 2, 3, 4, 5].map(function(x){return x * 2});
print(mapped);
這個示例的代碼與上例的作用是一樣的,不過我們不需要顯式的定義一個double函數(shù),只需要為map函數(shù)傳遞一個“可以將傳入?yún)?shù)乘2并返回”的代碼塊即可。再來看一個例子:
[
{id : "item1"},
{id : "item2"},
{id : "item3"}
].map(function(current){
print(current.id);
});
將會打?。?/p>
item1
item2
item3
也就是說,這個 map 的作用是將傳入的參數(shù)(處理器)應(yīng)用在數(shù)組中的每個元素上,而不關(guān)注數(shù)組元素的數(shù)據(jù)類型,數(shù)組的長度,以及處理函數(shù)的具體內(nèi)容。
C 語言中的函數(shù)指針,很容易實現(xiàn)一個高階函數(shù)。我們還以 map 為例,說明在 C 語言中如何實現(xiàn):
//prototype of function
void map(int* array, int length, int (*func)(int));
map 函數(shù)的第三個參數(shù)為一個函數(shù)指針,接受一個整型的參數(shù),返回一個整型參數(shù),我們來看看其實現(xiàn):
//implement of function map
void map(int* array, int length, int (*func)(int)){
int i = 0;
for(i = 0; i < length; i++){
array[i] = func(array[i]);
}
}
我們在這里實現(xiàn)兩個小函數(shù),分別計算傳入?yún)?shù)的乘 2 的值,和乘 3 的值,然后進行測試:
int twice(int num) { return num * 2; }
int triple(int num){ return num * 3; }
//function main
int main(int argc, char** argv){
int array[5] = {1, 2, 3, 4, 5};
int i = 0;
int len = 5;
//print the orignal array
printArray(array, len);
//mapped by twice
map(array, len, twice);
printArray(array, len);
//mapped by twice, then triple
map(array, len, triple);
printArray(array, len);
return 0;
}
運行結(jié)果如下:
1 2 3 4 5
2 4 6 8 10
6 12 18 24 30
應(yīng)該注意的是 map 的使用方法,如 map(array, len, twice)中,最后的參數(shù)為 twice,而 twice 為一個函數(shù)。因為 C 語言中,函數(shù)的定義不能嵌套,因此不能采用諸如 JavaScript 中的匿名函數(shù)那樣的簡潔寫法。
雖然在 C 語言中可以通過函數(shù)指針的方式來實現(xiàn)高階函數(shù),但是隨著高階函數(shù)的“階”的增高,指針層次勢必要跟著變得很復雜,那樣會增加代碼的復雜度,而且由于 C 語言是強類型的,因此在數(shù)據(jù)類型方面必然有很大的限制。
Java 中的匿名類,事實上可以理解成一個教笨重的閉包(可執(zhí)行單元),我們可以通過 Java 的匿名類來實現(xiàn)上述的 map 操作,首先,我們需要一個對函數(shù)的抽象:
interface Function{
int execute(int x);
}
我們假設(shè) Function 接口中有一個方法 execute,接受一個整型參數(shù),返回一個整型參數(shù),然后我們在類 List 中,實現(xiàn) map 操作:
private int[] array;
public List(int[] array){
this.array = array;
}
public void map(Function func){
for(int i = 0, len = this.array.length; i < len; i++){
this.array[i] = func.execute(this.array[i]);
}
}
map 接受一個實現(xiàn)了 Function 接口的類的實例,并調(diào)用這個對象上的 execute 方法來處理數(shù)組中的每一個元素。我們這里直接修改了私有成員 array,而并沒有創(chuàng)建一個新的數(shù)組。好了,我們來做個測試:
public static void main(String[] args){
List list = new List(new int[]{1, 2, 3, 4, 5});
list.print();
list.map(new Function(){
public int execute(int x){
return x * 2;
}
});
list.print();
list.map(new Function(){
public int execute(int x){
return x * 3;
}
});
list.print();
}
同前邊的兩個例子一樣,這個程序會打?。?/p>
1 2 3 4 5
2 4 6 8 10
6 12 18 24 30
灰色背景色的部分即為創(chuàng)建一個匿名類,從而實現(xiàn)高階函數(shù)。很明顯,我們需要傳遞給map的是一個可以執(zhí)行 execute 方法的代碼。而由于 Java 是命令式的編程語言,函數(shù)并非第一位的,函數(shù)必須依賴于對象,附屬于對象,因此我們不得不創(chuàng)建一個匿名類來包裝這個 execute 方法。而在 JavaScript 中,我們只需要傳遞函數(shù)本身即可,這樣完全合法,而且代碼更容易被人理解。
閉包和柯里化都是 JavaScript 經(jīng)常用到而且比較高級的技巧,所有的函數(shù)式編程語言都支持這兩個概念,因此,我們想要充分發(fā)揮出 JavaScript 中的函數(shù)式編程特征,就需要深入的了解這兩個概念,我們在第七章中詳細的討論了閉包及其特征,閉包事實上更是柯里化所不可缺少的基礎(chǔ)。
閉包的我們之前已經(jīng)接觸到,先說說柯里化??吕锘褪穷A先將函數(shù)的某些參數(shù)傳入,得到一個簡單的函數(shù),但是預先傳入的參數(shù)被保存在閉包中,因此會有一些奇特的特性。比如:
var adder = function(num){
return function(y){
return num + y;
}
}
var inc = adder(1);
var dec = adder(-1);
這里的 inc/dec 兩個變量事實上是兩個新的函數(shù),可以通過括號來調(diào)用,比如下例中的用法:
//inc, dec現(xiàn)在是兩個新的函數(shù),作用是將傳入的參數(shù)值(+/-)1
print(inc(99));//100
print(dec(101));//100
print(adder(100)(2));//102
print(adder(2)(100));//102
根據(jù)柯里化的特性,我們可以寫出更有意思的代碼,比如在前端開發(fā)中經(jīng)常會遇到這樣的情況,當請求從服務(wù)端返回后,我們需要更新一些特定的頁面元素,也就是局部刷新的概念。使用局部刷新非常簡單,但是代碼很容易寫成一團亂麻。而如果使用柯里化,則可以很大程度上美化我們的代碼,使之更容易維護。我們來看一個例子:
//update會返回一個函數(shù),這個函數(shù)可以設(shè)置id屬性為item的web元素的內(nèi)容
function update(item){
return function(text){
$("div#"+item).html(text);
}
}
//Ajax請求,當成功是調(diào)用參數(shù)callback
function refresh(url, callback){
var params = {
type : "echo",
data : ""
};
$.ajax({
type:"post",
url:url,
cache:false,
async:true,
dataType:"json",
data:params,
//當異步請求成功時調(diào)用
success: function(data, status){
callback(data);
},
//當請求出現(xiàn)錯誤時調(diào)用
error: function(err){
alert("error : "+err);
}
});
}
refresh("action.do?target=news", update("newsPanel"));
refresh("action.do?target=articles", update("articlePanel"));
refresh("action.do?target=pictures", update("picturePanel"));
其中,update 函數(shù)即為柯里化的一個實例,它會返回一個函數(shù),即:
update("newsPanel") = function(text){
$("div#newsPanel").html(text);
}
由于update(“newsPanel”)的返回值為一個函數(shù),需要的參數(shù)為一個字符串,因此在refresh的Ajax調(diào)用中,當success時,會給callback傳入服務(wù)器端返回的數(shù)據(jù)信息,從而實現(xiàn)newsPanel面板的刷新,其他的文章面板articlePanel,圖片面板picturePanel的刷新均采取這種方式,這樣,代碼的可讀性,可維護性均得到了提高。
通常來講,函數(shù)式編程的謂詞(關(guān)系運算符,如大于,小于,等于的判斷等),以及運算(如加減乘數(shù)等)都會以函數(shù)的形式出現(xiàn),比如:
a > b
通常表示為:
gt(a, b)//great than
因此,可以首先對這些常見的操作進行一些包裝,以便于我們的代碼更具有“函數(shù)式”風格:
function abs(x){ return x>0?x:-x;}
function add(a, b){ return a+b; }
function sub(a, b){ return a-b; }
function mul(a, b){ return a*b; }
function div(a, b){ return a/b; }
function rem(a, b){ return a%b; }
function inc(x){ return x + 1; }
function dec(x){ return x - 1; }
function equal(a, b){ return a==b; }
function great(a, b){ return a>b; }
function less(a, b){ return a<b; }
function negative(x){ return x<0; }
function positive(x){ return x>0; }
function sin(x){ return Math.sin(x); }
function cos(x){ return Math.cos(x); }
如果我們之前的編碼風格是這樣:
// n*(n-1)*(n-2)*...*3*2*1
function factorial(n){
if(n == 1){
return 1;
}else{
return n * factorial(n - 1);
}
}
在函數(shù)式風格下,就應(yīng)該是這樣了:
function factorial(n){
if(equal(n, 1)){
return 1;
}else{
return mul(n, factorial(dec(n)));
}
}
函數(shù)式編程的特點當然不在于編碼風格的轉(zhuǎn)變,而是由更深層次的意義。比如,下面是另外一個版本的階乘實現(xiàn):
/*
* product <- counter * product
* counter <- counter + 1
* */
function factorial(n){
function fact_iter(product, counter, max){
if(great(counter, max)){
return product;
}else{
fact_iter(mul(counter, product), inc(counter), max);
}
}
return fact_iter(1, 1, n);
}
雖然代碼中已經(jīng)沒有諸如+/-/*//之類的操作符,也沒有>,<,==,之類的謂詞,但是,這個函數(shù)仍然算不上具有函數(shù)式編程風格,我們可以改進一下:
function factorial(n){
return (function factiter(product, counter, max){
if(great(counter, max)){
return product;
}else{
return factiter(mul(counter, product), inc(counter), max);
}
})(1, 1, n);
}
factorial(10);
通過一個立即運行的函數(shù) factiter,將外部的 n 傳遞進去,并立即參與計算,最終返回運算結(jié)果。
提到遞歸,函數(shù)式語言中還有一個很有意思的主題,即:如果一個函數(shù)是匿名函數(shù),能不能進行遞歸操作呢?如何可以,怎么做?我們還是來看階乘的例子:
function factorial(x){
return x == 0 ? 1 : x * factorial(x-1);
}
factorial 函數(shù)中,如果 x 值為 0,則返回1,否則遞歸調(diào)用 factorial,參數(shù)為 x 減 1,最后當 x 等于 0 時進行規(guī)約,最終得到函數(shù)值(事實上,命令式程序語言中的遞歸的概念最早即來源于函數(shù)式編程中)?,F(xiàn)在考慮:將 factorial 定義為一個匿名函數(shù),那么在函數(shù)內(nèi)部,在代碼x*factorial(x-1)的地方,這個 factorial 用什么來替代呢?
lambda 演算的先驅(qū)們,天才的發(fā)明了一個神奇的函數(shù),成為 Y- 結(jié)合子。使用 Y- 結(jié)合子,可以做到對匿名函數(shù)使用遞歸。關(guān)于 Y- 結(jié)合子的發(fā)現(xiàn)及推導過程的討論已經(jīng)超出了本部分的范圍,有興趣的讀者可以參考附錄中的資料。我們來看看這個神奇的 Y- 結(jié)合子:
var Y = function(f) {
return (function(g) {
return g(g);
})(function(h) {
return function() {
return f(h(h)).apply(null, arguments);
};
});
};
我們來看看如何運用 Y-結(jié)合子,依舊是階乘這個例子:
var factorial = Y(function(func){
return function(x){
return x == 0 ? 1 : x * func(x-1);
}
});
factorial(10);
或者:
Y(function(func){
return function(x){
return x == 0 ? 1 : x * func(x-1);
}
})(10);
不要被上邊提到的 Y-結(jié)合子的表達式嚇到,事實上,在 JavaScript 中,我們有一種簡單的方法來實現(xiàn) Y-結(jié)合子:
var fact = function(x){
return x == 0 : 1 : x * arguments.callee(x-1);
}
fact(10);
或者:
(function(x){
return x == 0 ? 1 : x * arguments.callee(x-1);
})(10);//3628800
其中,arguments.callee 表示函數(shù)的調(diào)用者,因此省去了很多復雜的步驟。
下面的代碼則頗有些“開發(fā)智力”之功效:
//函數(shù)的不動點
function fixedPoint(fx, first){
var tolerance = 0.00001;
function closeEnough(x, y){return less( abs( sub(x, y) ), tolerance)};
function Try(guess){//try 是javascript中的關(guān)鍵字,因此這個函數(shù)名為大寫
var next = fx(guess);
//print(next+" "+guess);
if(closeEnough(guess, next)){
return next;
}else{
return Try(next);
}
};
return Try(first);
}
// 數(shù)層嵌套函數(shù),
function sqrt(x){
return fixedPoint(
function(y){
return function(a, b){ return div(add(a, b),2);}(y, div(x, y));
},
1.0);
}
print(sqrt(100));
fiexedPoint 求函數(shù)的不動點,而 sqrt 計算數(shù)值的平方根。這些例子來源于《計算機程序的構(gòu)造和解釋》,其中列舉了大量的計算實例,不過該書使用的是 scheme 語言,在本書中,例子均被翻譯為 JavaScript。