1 函數語言程式設計簡介
函數語言程式設計是和傳統指令式程式設計區分的一種程式設計思想,“在函數語言程式設計語言中,函式是第一類的物件,也就是說,函式 不依賴於任何其他的物件而可以獨立存在,而在物件導向的語言中,函式 ( 方法 ) 是依附於物件的,屬於物件的一部分。這一點決定了函式在函式式語言中的一些特別的性質,比如作為傳出 / 傳入引數,作為一個普通的變數等。[1]”
函數語言程式設計思想的源頭可以追溯到 20 世紀 30 年代,數學家阿隆左 . 丘奇在進行一項關於問題的可計算性的研究,也就是後來的 lambda 演算。lambda 演算的本質為 一切皆函式,函式可以作為另外一個函式的輸出或者 / 和輸入,一系列的函式使用最終會形成一個表示式鏈,這個表示式鏈可以最終求得一個值,而這個過程,即為計算的本質。
然而,這種思想在當時的硬體基礎上很難實現,歷史最終選擇了同丘奇的 lambda 理論平行的另一種數學理論:圖靈機作為計算理論,而採取另一位科學家馮 . 諾依曼的計算機結構,並最終被實現為硬體。由於第一臺計算機即為馮 . 諾依曼的程式儲存結構,因此執行在此平臺的程式也繼承了這種基因,程式設計語言如 C/Pascal 等都在一定程度上依賴於此體系。
到了 20 世紀 50 年代,一位 MIT 的教授 John McCarthy 在馮 . 諾依曼體系的機器上成功的實現了 lambda 理論,取名為 LISP(LISt Processor), 至此函數語言程式設計語言便開始活躍於電腦科學領域。
2 JavaScript中的函數語言程式設計思想
2.1)匿名函式
在函數語言程式設計語言中,函式是可以沒有名字的,匿名函式通常表示:“可以完成某件事的一塊程式碼”。這種表達在很多場合是有用的,因為我們有時需要用函式完成某件事,但是這個函式可能只是臨時性的,那就沒有理由專門為其生成一個頂層的函式物件。
//自定義map()
function map(array, func){
var res = [];
for ( var i = 0, len = array.length; i < len; i++){
res.push(func(array[i]));
}
return res;
}
//匿名函式
var mapped = map([1, 3, 5, 7, 8], function (n){
return n = n + 1;
});
alert(mapped); //2,4,6,8,9// 對陣列 [1,3,5,7,8] 中每一個元素加 1
2.2)柯里化
是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,或者說建立一個已經預先包含一個引數的函式,這其中會使用到閉包。
先來一個不是類似克里化的例子:
function add(num1, num2){
return num1 + num2;
}
function curriedAdd(num2){
return add(5, num2);
}
alert(add(2, 3)); //5
alert(curriedAdd(3)); //8
這裡的curriedAdd函式類似克里化,因為它固定了函式的一個引數。
下面看一個真正的函式克里化:
//傳入的引數不止fn一個,但是隻有fn顯式用到,所以明確給出
function curry(fn){
var args = Array.prototype.slice.call(arguments, 1);//得到外部函式引數
return function(){
var innerArgs = Array.prototype.slice.call(arguments);//得到內部函式引數
var finalArgs = args.concat(innerArgs);//引數合併得到整體引數陣列
return fn.apply(null, finalArgs);//對整體引數陣列運用函式fn
};
}
function add(num1, num2){
return num1 + num2;
}
var curriedAdd = curry(add, 5);
alert(curriedAdd(3)); //8
根據上述例項,克里化一般通過curry()函式動態實現,curry()函式的主要工作就是將被返回函式的引數進行排序。 curry()的第一個引數是要進行柯里化的函式,其他引數是要傳入的值。為了獲取第一個引數之後的所有引數,在 arguments 物件上呼叫了 slice()方法,並傳入引數 1 表示被返回的陣列包含從第二個引數開始的所有引數。然後 args 陣列包含了來自外部函式的引數。在內部函式中,建立了 innerArgs 陣列用來存放所有傳入的引數(又一次用到了 slice())。有了存放來自外部函式和內部函式的引數陣列後,就可以使用 concat()方法將它們組合為 finalArgs,然後使用 apply()將結果傳遞給該函式。注意這個函式並沒有考慮到執行環境,所以呼叫 apply()時第一個引數是 null。
2.3)高階函式
即為對函式的進一步抽象,map(array, func) 的表示式已經表明,將 func 函式作用於 array 中的每一個元素,最終返回一個新的 array,應該注意的是,map 對 array 和 func 的實現是沒有任何預先的假設的(抽象),因此稱之為“高階”函式。
function map(array, func){
var res = [];
for ( var i = 0, len = array.length; i < len; i++){
res.push(func(array[i]));
}
return res;
}
var mapped = map([1, 3, 5, 7, 8], function (n){
return n = n + 1;
});
alert(mapped);
var mapped2 = map(["one", "two" , "three", "four"],
function (item){
return "("+item+")";
});
alert(mapped2);
mapped 和 mapped2 均呼叫了 map,但是得到了截然不同的結果,因為 map 的引數本身已經進行了一次抽象,map 函式做的是第二次抽象,高階的“階”可以理解為抽象的層次。
2.4)閉包
閉包已經在另一篇文章《閉包:私有化變數》中介紹過,與閉包有關的垃圾回收機制在《垃圾回收機制——總結自《JavaScript高階程式設計》也有詳細介紹,因此這裡僅僅簡要說明閉包。
當在一個函式 outter 內部定義另一個函式 inner,而 inner 又引用了 outter 作用域內的變數,在 outter 之外使用 inner 函式,則形成了閉包。
<script type="text/javascript">
function outter(){
var n = 0;
return function (){
return n++;
}
}
var o1 = outter();
o1();//n == 0
o1();//n == 1
alert(o1());//n == 2
var o2 = outter();
o2();//n == 0
alert(o2());//n == 1
</script>
匿名函式 function(){return n++;} 中包含對 outter 的區域性變數 n 的引用,因此當 outter 返回時,n 的值被保留 ( 不會被垃圾回收機制回收 ),持續呼叫 o1(),將會改變 n 的值。而 o2 的值並不會隨著 o1() 被呼叫而改變,第一次呼叫 o2 會得到 n==0 的結果,用物件導向的術語來說,就是 o1 和 o2 為不同的 例項,互不干涉。
閉包例項2:
//建立一個物件並壓入陣列
var outter = [];
function clouseTest () {
var array = [“one”, “two”, “three”, “four”];
for ( var i = 0; i < array.length;i++){
var x = {};
x.no = i;
x.text = array[i];
x.invoke = function (){
alert(i);
}
outter.push(x);
}
}
//執行函式並呼叫每個陣列元素對應的物件的方法
clouseTest();// 呼叫這個函式,向 outter 陣列中新增物件
for ( var i = 0, len = outter.length; i < len; i++){
outter[i].invoke(); //4,4,4,4
}
由於 i 是閉包中的區域性變數,for 迴圈最後退出時的值為 4,因此呼叫 outter 中的每個元素都會得到 4。
修正過後的閉包:
<script type="text/javascript">
//建立一個物件並壓入陣列
var outter = [];
function clouseTest () {
var array = ["one", "two", "three", "four"];
for ( var i = 0; i < array.length;i++){
var x = {};
x.no = i;
x.text = array[i];
//返回匿名函式,匿名函式中包含一個閉包,後續的括號表示立即執行
x.invoke = function (no){
return function (){
alert(no);
}
}(i); //這個i是匿名函式的引數,會賦值給匿名函式的引數no,這個i導致了不同的no副本
outter.push(x);
}
}
//執行函式並呼叫每個陣列元素對應的物件的方法
clouseTest();// 呼叫這個函式,向 outter 陣列中新增物件
for ( var i = 0, len = outter.length; i < len; i++){
outter[i].invoke(); //1,2,3,4
}
</script>