您好,登錄后才能下訂單哦!
和Lisp、Haskell不同,javascript并非函數(shù)式編程語言,但在javascript中可以操控對(duì)象一樣操控函數(shù),也就是說可以在javascript中應(yīng)用函數(shù)式編程技術(shù)。ES5中的數(shù)組方法(如map()和reduce())就可以非常適合用于函數(shù)式編程風(fēng)格。本文將詳細(xì)介紹函數(shù)式編程
假設(shè)有一個(gè)數(shù)組,數(shù)組元素都是數(shù)字,想要計(jì)算這些元素的平均值和標(biāo)準(zhǔn)差。若使用非函數(shù)式編程風(fēng)格的話,如下所示
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));
可以使用數(shù)組方法map()和reduce()實(shí)現(xiàn)同樣的計(jì)算,這種實(shí)現(xiàn)極其簡潔
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中,并不包含這些數(shù)組方法,需要自定義map()和reduce()函數(shù)
//對(duì)于每個(gè)數(shù)組元素調(diào)用函數(shù)f(),并返回一個(gè)結(jié)果數(shù)組//如果Array.prototype.map定義了的話,就使用這個(gè)方法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; }
//使用函數(shù)f()和可選的初始值將數(shù)組a減到一個(gè)值//如果Array.prototype.reduce存在的話,就使用這個(gè)方法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; }
高階函數(shù)(higher-order function)指操作函數(shù)的函數(shù),它接收一個(gè)或多個(gè)函數(shù)作為參數(shù),并返回一個(gè)新函數(shù)
//這個(gè)高階函數(shù)返回一個(gè)新的函數(shù),這個(gè)新函數(shù)將它的實(shí)參傳入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()函數(shù)就是一個(gè)高階函數(shù),因?yàn)樗邮找粋€(gè)函數(shù)作為參數(shù),并返回一個(gè)新函數(shù)
下面的mapper()函數(shù),也是接收一個(gè)函數(shù)作為參數(shù),并返回一個(gè)新函數(shù),這個(gè)新函數(shù)將一個(gè)數(shù)組映射到另一個(gè)使用這個(gè)函數(shù)的數(shù)組上
//所返回的函數(shù)的參數(shù)應(yīng)當(dāng)是一個(gè)實(shí)參數(shù)組,并對(duì)每個(gè)數(shù)組元素執(zhí)行函數(shù)f(),并返回所有計(jì)算結(jié)果組成的數(shù)組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]
下面是一個(gè)更常見的例子,它接收兩個(gè)函數(shù)f()和g(),并返回一個(gè)新函數(shù)用以計(jì)算f(g())
//返回一個(gè)新的可以計(jì)算f(g(...))的函數(shù)//返回的函數(shù)h()將它所有的實(shí)參傳入g(),然后將g()的返回值傳入f()//調(diào)用f()和g()時(shí)的this值和調(diào)用h()時(shí)的this值是同一個(gè)thisfunction compose(f,g){ return function(){ //需要給f()傳入一個(gè)參數(shù),所以使用f()的call()方法 //需要給g()傳入很多參數(shù),所以使用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
不完全函數(shù)是一種函數(shù)變換技巧,即把一次完整的函數(shù)調(diào)用拆成多次函數(shù)調(diào)用,每次傳入的實(shí)參都是完整實(shí)參的一部分,每個(gè)拆分開的函數(shù)叫做不完全函數(shù),每次函數(shù)調(diào)用叫做不完全調(diào)用。這種函數(shù)變換的特點(diǎn)是每次調(diào)用都返回一個(gè)函數(shù),直到得到最終運(yùn)行結(jié)果為止
函數(shù)f()的bind()方法返回一個(gè)新函數(shù),給新函數(shù)傳入特定的上下文和一組指定的參數(shù),然后調(diào)用函數(shù)f()。bind()方法只是將實(shí)參放在完整實(shí)參列表的左側(cè),也就是說傳入bind()的實(shí)參都是放在傳入原始函數(shù)的實(shí)參列表開始的位置,但有時(shí)希望將傳入bind()的實(shí)參放在完整實(shí)參列表的右側(cè)
//實(shí)現(xiàn)一個(gè)工具函數(shù)將類數(shù)組對(duì)象(或?qū)ο?轉(zhuǎn)換為真正的數(shù)組function array(a,n){ return Array.prototype.slice.call(a,n||0); }//這個(gè)函數(shù)的實(shí)參傳遞到左側(cè)function partialLeft(f){ var args = arguments; return function(){ var a = array(args,1); a = a.concat(array(arguments)); return f.apply(this,a); }; }//這個(gè)函數(shù)的實(shí)參傳遞到右側(cè)function partialRight(f){ var args = arguments; return function(){ var a = array(arguments); a = a.concat(array(args,1)); return f.apply(this,a); }; }//這個(gè)函數(shù)的實(shí)參被用作模板,實(shí)參列表中的undefined值都被填充function partial(f){ var args = arguments; return function(){ var a = array(args,1); var i = 0, j = 0; //遍歷args,從內(nèi)部實(shí)參填充undefined值 for(;i<a.length;i++){ if(a[i] === undefined){ a[i] = arguments[j++]; } //現(xiàn)在將剩下的內(nèi)部實(shí)參都追加進(jìn)去 }; a = a.concat(array(arguments,j)); return f.apply(this,a); } }//這個(gè)函數(shù)有三個(gè)實(shí)參var f = function(x,y,z){ return x*(y - z); }//注意這三個(gè)不完全調(diào)用之間的區(qū)別partialLeft(f,2)(3,4);//2*(3-4)=-2partialRight(f,2)(3,4);//3*(4-2)=6partial(f,undefined,2)(3,4);//3*(2-4)=-6
利用這種不完全函數(shù)的編程技巧,可以編寫一些有意思的代碼,利用已有的函數(shù)來定義新的函數(shù)
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);
當(dāng)將不完全調(diào)用和其他高階函數(shù)整合在一起時(shí),事件就變得格外有趣了。比如,下例定義了not()函數(shù)
var not = partialLeft(compose,function(x){ return !x; });var even = function(x){ return x % 2 === 0; };var odd = not(even);var isNumber = not(isNaN);
可以使用不完全調(diào)用的組合來重新組織求平均數(shù)和標(biāo)準(zhǔn)差的代碼,這種編碼風(fēng)格是非常純粹的函數(shù)式編程
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))));
將上次的計(jì)算結(jié)果緩存起來,在函數(shù)式編程中,這種緩存技巧叫做記憶(memorization)。記憶只是一種編程技巧,本質(zhì)上是犧牲算法的空間復(fù)雜度以換取更優(yōu)的時(shí)間復(fù)雜度,在客戶端javascript中代碼的執(zhí)行時(shí)間復(fù)雜度往往成為瓶頸,因此在大多數(shù)場景下,這種犧牲空間換取時(shí)間的做法以提升程序執(zhí)行效率的做法是非??扇〉?/p>
//返回f()的帶有記憶功能的版本//只有當(dāng)f()的實(shí)參的字符串表示都不相同時(shí)它才會(huì)工作function memorize(f){ var cache = {};//將值保存到閉包內(nèi) return function(){ //將實(shí)參轉(zhuǎn)換為字符串形式,并將其用做緩存的鍵 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()函數(shù)創(chuàng)建一個(gè)新的對(duì)象,這個(gè)對(duì)象被當(dāng)作緩存的宿主,并賦值給一個(gè)局部變量,因此對(duì)于返回的函數(shù)來說它是私有的。所返回的函數(shù)將它的實(shí)參數(shù)組轉(zhuǎn)換成字符串,并將字符串用做緩存對(duì)象的屬性名。如果在緩存中存在這個(gè)值,則直接返回它;否則,就調(diào)用既定的函數(shù)對(duì)實(shí)參進(jìn)行計(jì)算,將計(jì)算結(jié)果緩存起來并返回
//返回兩個(gè)整數(shù)的最大公約數(shù)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
寫一個(gè)遞歸函數(shù)時(shí),往往需要實(shí)現(xiàn)記憶功能,我們更希望調(diào)用實(shí)現(xiàn)了記憶功能的遞歸函數(shù),而不是原遞歸函數(shù)
var factorial = memorize(function(n){ return (n<=1) ? 1 : n*factorial(n-1); }); factorial(5);//120
下面利用連續(xù)調(diào)用單參函數(shù)來實(shí)現(xiàn)一個(gè)簡易的加法運(yùn)算
add(num1)(num2)(num3)…; add(10)(10) = 20add(10)(20)(50) = 80add(10)(20)(50)(100) = 180
如果完全按照上面實(shí)現(xiàn),則無法實(shí)現(xiàn),因?yàn)閍dd(1)(2)如果返回3,add(1)(2)(3)必然報(bào)錯(cuò)。于是,有以下兩種變形方法
第一種變形如下:
add(num1)(num2)(num3)…; add(10)(10)() = 20add(10)(20)(50)() = 80add(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)());//10console.log(add(10)(10)());//20console.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));//10console.log(+add(10)(10));//20console.log(+add(10)(10)(10));//30
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。