前面的話
和Lisp、Haskell不同,javascript並非函數式編程語言,但在javascript中可以操控對象一樣操控函數,也就是說可以在javascript中應用函數式編程技術。ES5中的數組方法(如map()和reduce())就可以非常適合用於函數式編程風格。本文將詳細介紹函數式編程
函數處理數組
假設有一個數組,數組元素都是數字,想要計算這些元素的平均值和標準差。若使用非函數式編程風格的話,如下所示
var data = [1,1,3,5,5];
var total = 0;
for(var i = 0 ; i < data.length; i++){
total += data[i];
}
var mean = total/data.length;
total = 0;
for(var i = 0; i < data.length; i++){
var deviation = data[i] - mean;
total += deviation * deviation;
}
var stddev = Math.sqrt(total/(data.length-1));
可以使用數組方法map()和reduce()實現同樣的計算,這種實現極其簡潔
var sum = function(x,y){
return x+y;
}
var square = function(x){
return x*x;
}
var data = [1,1,3,5,5];
var mean = data.reduce(sum)/data.length;
var deviations = data.map(function(x){
return x - mean;
});
var stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1));
在ES3中,並不包含這些數組方法,需要自定義map()和reduce()函數
//對於每個數組元素調用函數f(),並返回一個結果數組
//如果Array.prototype.map定義了的話,就使用這個方法
var map = Array.prototype.map ? function(a,f){return a.map(f);}
: function (a,f){
var results = [];
for(var i = 0,len=a.length; i < len; i++){
if(i in a){
results[i] = f.call(null,a[i],i,a);
}
}
return results;
}
//使用函數f()和可選的初始值將數組a減到一個值
//如果Array.prototype.reduce存在的話,就使用這個方法
var reduce = Array.prototype.reduce
? function(a,f,initial){
if(arguments.length > 2){
return a.reduce(f,initial);
}else{
return a.reduce(f);
}
}
: function(a,f,initial){
var i = 0, len = a.length ,accumulator;
if(argument.length > 2){
accumulator = initial;
}else{
if(len == 0){
throw TypeError();
}
while(i < len){
if(i in a){
accumulator = a[i++];
break;
}else{
i++;
}
}
if(i == len){
throw TypeError();
}
}
while(i < len){
if(i in a){
accumulator = f.call(undefined,accumulator,a[i],i,a);
}
i++;
}
return accumulator;
}
高階函數
高階函數(higher-order function)指操作函數的函數,它接收一個或多個函數作爲參數,並返回一個新函數
//這個高階函數返回一個新的函數,這個新函數將它的實參傳入f(),並返回f的返回值的邏輯非
function not(f){
return function(){
var result = f.apply(this,arguments);
return !result;
};
}
var even = function(x){
return x % 2 === 0;
}
var odd = not(even);
[1,1,3,5,5].every(odd);//true
上面的not()函數就是一個高階函數,因爲它接收一個函數作爲參數,並返回一個新函數
下面的mapper()函數,也是接收一個函數作爲參數,並返回一個新函數,這個新函數將一個數組映射到另一個使用這個函數的數組上
//所返回的函數的參數應當是一個實參數組,並對每個數組元素執行函數f(),並返回所有計算結果組成的數組
function mapper(f){
return function(a){
return map(a,f);
}
}
var increment = function(x){
return x+1;
}
var incrementer = mapper(increment);
increment([1,2,3]);//[2,3,4]
下面是一個更常見的例子,它接收兩個函數f()和g(),並返回一個新函數用以計算f(g())
//返回一個新的可以計算f(g(...))的函數
//返回的函數h()將它所有的實參傳入g(),然後將g()的返回值傳入f()
//調用f()和g()時的this值和調用h()時的this值是同一個this
function compose(f,g){
return function(){
//需要給f()傳入一個參數,所以使用f()的call()方法
//需要給g()傳入很多參數,所以使用g()的apply()方法
return f.call(this,g.apply(this,arguments));
};
}
var square = function(x){
return x*x;
}
var sum = function(x,y){
return x + y;
}
var squareofsum = compose(square,sum);
squareofsum(2,3);//25
不完全函數
不完全函數是一種函數變換技巧,即把一次完整的函數調用拆成多次函數調用,每次傳入的實參都是完整實參的一部分,每個拆分開的函數叫做不完全函數,每次函數調用叫做不完全調用。這種函數變換的特點是每次調用都返回一個函數,直到得到最終運行結果爲止
函數f()的bind()方法返回一個新函數,給新函數傳入特定的上下文和一組指定的參數,然後調用函數f()。bind()方法只是將實參放在完整實參列表的左側,也就是說傳入bind()的實參都是放在傳入原始函數的實參列表開始的位置,但有時希望將傳入bind()的實參放在完整實參列表的右側
//實現一個工具函數將類數組對象(或對象)轉換爲真正的數組
function array(a,n){
return Array.prototype.slice.call(a,n||0);
}
//這個函數的實參傳遞到左側
function partialLeft(f){
var args = arguments;
return function(){
var a = array(args,1);
a = a.concat(array(arguments));
return f.apply(this,a);
};
}
//這個函數的實參傳遞到右側
function partialRight(f){
var args = arguments;
return function(){
var a = array(arguments);
a = a.concat(array(args,1));
return f.apply(this,a);
};
}
//這個函數的實參被用作模板,實參列表中的undefined值都被填充
function partial(f){
var args = arguments;
return function(){
var a = array(args,1);
var i = 0, j = 0;
//遍歷args,從內部實參填充undefined值
for(;i<a.length;i++){
if(a[i] === undefined){
a[i] = arguments[j++];
}
//現在將剩下的內部實參都追加進去
};
a = a.concat(array(arguments,j));
return f.apply(this,a);
}
}
//這個函數有三個實參
var f = function(x,y,z){
return x*(y - z);
}
//注意這三個不完全調用之間的區別
partialLeft(f,2)(3,4);//2*(3-4)=-2
partialRight(f,2)(3,4);//3*(4-2)=6
partial(f,undefined,2)(3,4);//3*(2-4)=-6
利用這種不完全函數的編程技巧,可以編寫一些有意思的代碼,利用已有的函數來定義新的函數
var increment = partialLeft(sum,1);
var cuberoot = partialRight(Math.pow,1/3);
String.prototype.first = partial(String.prototype.charAt,0);
String.prototype.last = partial(String.prototype.substr,-1,1);
當將不完全調用和其他高階函數整合在一起時,事件就變得格外有趣了。比如,下例定義了not()函數
var not = partialLeft(compose,function(x){
return !x;
});
var even = function(x){
return x % 2 === 0;
};
var odd = not(even);
var isNumber = not(isNaN);
可以使用不完全調用的組合來重新組織求平均數和標準差的代碼,這種編碼風格是非常純粹的函數式編程
var data = [1,1,3,5,5];
var sum = function(x,y){return x+y;}
var product = function(x,y){return x*y;}
var neg = partial(product,-1);
var square = partial(Math.pow,undefined,2);
var sqrt = partial(Math.pow,undefined,.5);
var reciprocal = partial(Math.pow,undefined,-1);
var mean = product(reduce(data,sum),reciprocal(data.length));
var stddev = sqrt(product(reduce(map(data,compose(square,partial(sum,neg(mean)))),sum),reciprocal(sum(data.length,-1))));
記憶
將上次的計算結果緩存起來,在函數式編程中,這種緩存技巧叫做記憶(memorization)。記憶只是一種編程技巧,本質上是犧牲算法的空間複雜度以換取更優的時間複雜度,在客戶端javascript中代碼的執行時間複雜度往往成爲瓶頸,因此在大多數場景下,這種犧牲空間換取時間的做法以提升程序執行效率的做法是非常可取的
//返回f()的帶有記憶功能的版本
//只有當f()的實參的字符串表示都不相同時它纔會工作
function memorize(f){
var cache = {};//將值保存到閉包內
return function(){
//將實參轉換爲字符串形式,並將其用做緩存的鍵
var key = arguments.length + Array.prototype.join.call(arguments ,",");
if(key in cache){
return cache[key];
}else{
return cache[key] = f.apply(this,arguments);
}
}
}
memorize()函數創建一個新的對象,這個對象被當作緩存的宿主,並賦值給一個局部變量,因此對於返回的函數來說它是私有的。所返回的函數將它的實參數組轉換成字符串,並將字符串用做緩存對象的屬性名。如果在緩存中存在這個值,則直接返回它;否則,就調用既定的函數對實參進行計算,將計算結果緩存起來並返回
//返回兩個整數的最大公約數
function gcd(a,b){
var t;
if(a < b){
t = b, b = a, a = t;
}
while(b != 0){
t = b, b = a % b, a = t;
}
return a;
}
var gcdmemo = memorize(gcd);
gcdmemo(85,187);//17
寫一個遞歸函數時,往往需要實現記憶功能,我們更希望調用實現了記憶功能的遞歸函數,而不是原遞歸函數
var factorial = memorize(function(n){
return (n<=1) ? 1 : n*factorial(n-1);
});
factorial(5);//120
連續調用單參函數
下面利用連續調用單參函數來實現一個簡易的加法運算
add(num1)(num2)(num3)…;
add(10)(10) = 20
add(10)(20)(50) = 80
add(10)(20)(50)(100) = 180
如果完全按照上面實現,則無法實現,因爲add(1)(2)如果返回3,add(1)(2)(3)必然報錯。於是,有以下兩種變形方法
第一種變形如下:
add(num1)(num2)(num3)…;
add(10)(10)() = 20
add(10)(20)(50)() = 80
add(10)(20)(50)(100)() = 180
function add(n){
return function f(m){
if(m === undefined){
return n;
}else{
n += m;
return f;
}
}
}
console.log(add(10)());//10
console.log(add(10)(10)());//20
console.log(add(10)(10)(10)());//30
第二種變形如下:
add(num1)(num2)(num3)…;
+add(10)(10) = 20
+add(10)(20)(50) = 80
+add(10)(20)(50)(100) = 180
function add(n) {
function f(m){
n += m;
return f;
};
f.toString = f.valueOf = function () {return n}
return f;
}
console.log(+add(10));//10
console.log(+add(10)(10));//20
console.log(+add(10)(10)(10));//30