zl程序教程

您现在的位置是:首页 >  前端

当前栏目

JavaScript函数式编程实践(来自IBM)

JavaScript编程 函数 实践 IBM 来自
2023-06-13 09:14:23 时间

函数式编程简介
说到函数式编程,人们的第一印象往往是其学院派,晦涩难懂,大概只有那些蓬头散发,不修边幅,甚至有些神经质的大学教授们才会用的编程方式。这可能在历史上的某个阶段的确如此,但是近来函数式编程已经在实际应用中发挥着巨大作用了,而更有越来越多的语言不断的加入诸如闭包,匿名函数等的支持,从某种程度上来讲,函数式编程正在逐步“同化”命令式编程。
函数式编程思想的源头可以追溯到20世纪30年代,数学家阿隆左.丘奇在进行一项关于问题的可计算性的研究,也就是后来的lambda演算。lambda演算的本质为一切皆函数,函数可以作为另外一个函数的输出或者/和输入,一系列的函数使用最终会形成一个表达式链,这个表达式链可以最终求得一个值,而这个过程,即为计算的本质。
然而,这种思想在当时的硬件基础上很难实现,历史最终选择了同丘奇的lambda理论平行的另一种数学理论:图灵机作为计算理论,而采取另一位科学家冯.诺依曼的计算机结构,并最终被实现为硬件。由于第一台计算机即为冯.诺依曼的程序存储结构,因此运行在此平台的程序也继承了这种基因,程序设计语言如C/Pascal等都在一定程度上依赖于此体系。
到了20世纪50年代,一位MIT的教授JohnMcCarthy在冯.诺依曼体系的机器上成功的实现了lambda理论,取名为LISP(LIStProcessor),至此函数式编程语言便开始活跃于计算机科学领域。
函数式编程语言特性
在函数式编程语言中,函数是第一类的对象,也就是说,函数不依赖于任何其他的对象而可以独立存在,而在面向对象的语言中,函数(方法)是依附于对象的,属于对象的一部分。这一点j决定了函数在函数式语言中的一些特别的性质,比如作为传出/传入参数,作为一个普通的变量等。
区别于命令式编程语言,函数式编程语言具有一些专用的概念,我们分别进行讨论:
匿名函数
在函数式编程语言中,函数是可以没有名字的,匿名函数通常表示:“可以完成某件事的一块代码”。这种表达在很多场合是有用的,因为我们有时需要用函数完成某件事,但是这个函数可能只是临时性的,那就没有理由专门为其生成一个顶层的函数对象。比如:

清单1.map函数

复制代码代码如下:

functionmap(array,func){
varres=[];
for(vari=0,len=array.length;i<len;i++){
res.push(func(array[i]));
}
returnres;
}
varmapped=map([1,3,5,7,8],function(n){
returnn=n+1;
});
print(mapped);
运行这段代码,将会打印:
2,4,6,8,9//对数组[1,3,5,7,8]中每一个元素加1

注意map函数的调用,map的第二个参数为一个函数,这个函数对map的第一个参数(数组)中的每一个都有作用,但是对于map之外的代码可能没有任何意义,因此,我们无需为其专门定义一个函数,匿名函数已经足够。
柯里化
柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这句话有点绕口,我们可以通过例子来帮助理解:
清单2.柯里化函数
复制代码代码如下:

functionadder(num){
return
function(x){
returnnum+x;
}
}
varadd5=adder(5);
varadd6=adder(6);
print(add5(1));
print(add6(1));

结果为:
6
7
比较有意思的是:函数adder接受一个参数,并返回一个函数,这个返回的函数可以被预期的那样被调用。变量add5保持着adder(5)返回的函数,这个函数可以接受一个参数,并返回参数与5的和。
柯里化在DOM的回调中非常有用,我们将在下面的小节中看到。
高阶函数
高阶函数即为对函数的进一步抽象,事实上,我们在匿名函数小节提到的map函数即为一种高阶函数,在很多的函数式编程语言中均有此函数。map(array,func)的表达式已经表明,将func函数作用于array中的每一个元素,最终返回一个新的array,应该注意的是,map对array和func的实现是没有任何预先的假设的,因此称之为“高阶”函数:

清单3.高阶函数
复制代码代码如下:
functionmap(array,func){
varres=[];
for(vari=0,len=array.length;i<len;i++){
res.push(func(array[i]));
}
returnres;
}
varmapped=map([1,3,5,7,8],function(n){
returnn=n+1;
});
print(mapped);
varmapped2=map(["one","two","three","four"],
function(item){
return"("+item+")";
});
print(mapped2);


将会打印如下结果:
2,4,6,8,9
(one),(two),(three),(four)//为数组中的每个字符串加上括号

mapped和mapped2均调用了map,但是得到了截然不同的结果,因为map的参数本身已经进行了一次抽象,map函数做的是第二次抽象,高阶的“阶”可以理解为抽象的层次。
JavaScript中的函数式编程
JavaScript是一门被误解甚深的语言,由于早期的Web开发中,充满了大量的copy-paste代码,因此平时可以见到的JavaScript代码质量多半不高,而且JavaScript代码总是很飞动的不断闪烁的gif广告,限制网页内容的复制等联系在一起的,因此包括Web开发者在内的很多人根本不愿意去学习JavaScript。
这种情形在Ajax复兴时得到了彻底的扭转,GoogleMap,Gmail等Ajax应用的出现使人们惊叹:原来JavaScript还可以做这样的事!很快,大量优秀的JavaScript/Ajax框架不断出现,比如Dojo,Prototype,jQuery,ExtJS等等。这些代码在给页面带来绚丽的效果的同时,也让开发者看到函数式语言代码的优雅。
函数式编程风格
在JavaScript中,函数本身为一种特殊对象,属于顶层对象,不依赖于任何其他的对象而存在,因此可以将函数作为传出/传入参数,可以存储在变量中,以及一切其他对象可以做的事情(因为函数就是对象)。
JavaScript被称为有着C语法的LISP,LISP代码的一个显著的特点是大量的括号以及前置的函数名,比如:

清单4.LISP中的加法
(+134567)

加号在LISP中为一个函数,这条表达式的意思为将加号后边的所有数字加起来,并将值返回,JavaScript可以定义同样的求和函数:

清单5.JavaScript中的求和
复制代码代码如下:
functionsum(){
varres=0;
for(vari=0,len=arguments.length;i<len;i++){
res+=parseInt(arguments[i]);
}
returnres;
}
print(sum(1,2,3));
print(sum(1,2,3,4,6,7,8));

运行此段代码,得到如下结果:
6
31

如果要完全模拟函数式编码的风格,我们可以定义一些诸如:

清单6.一些简单的函数抽象
复制代码代码如下:
functionadd(a,b){returna+b;}
functionsub(a,b){returna-b;}
functionmul(a,b){returna*b;}
functiondiv(a,b){returna/b;}
functionrem(a,b){returna%b;}
functioninc(x){returnx+1;}
functiondec(x){returnx-1;}
functionequal(a,b){returna==b;}
functiongreat(a,b){returna>b;}
functionless(a,b){returna<b;}

这样的小函数以及谓词,那样我们写出的代码就更容易被有函数式编程经验的人所接受:


清单7.函数式编程风格
复制代码代码如下:
//修改之前的代码
functionfactorial(n){
if(n==1){
return1;
}else{
returnfactorial(n-1)*n;
}
}
//更接近“函数式”编程风格的代码
functionfactorial(n){
if(equal(n,1)){
return1;
}else{
returnmul(n,factorial(dec(n)));
}
}

闭包及其使用
闭包是一个很有趣的主题,当在一个函数outter内部定义另一个函数inner,而inner又引用了outter作用域内的变量,在outter之外使用inner函数,则形成了闭包。描述起来虽然比较复杂,在实际编程中却经常无意的使用了闭包特性。

清单8.一个闭包的例子
复制代码代码如下:
functionoutter(){
varn=0;
return
function(){
returnn++;
}
}
varo1=outter();
o1();//n==0
o1();//n==1
o1();//n==2
varo2=outter();
o2();//n==0
o2();//n==1

匿名函数function(){returnn++;}中包含对outter的局部变量n的引用,因此当outter返回时,n的值被保留(不会被垃圾回收机制回收),持续调用o1(),将会改变n的值。而o2的值并不会随着o1()被调用而改变,第一次调用o2会得到n==0的结果,用面向对象的术语来说,就是o1和o2为不同的实例,互不干涉。
总的来说,闭包很简单,不是吗?但是,闭包可以带来很多好处,比如我们在Web开发中经常用到的:

清单9.jQuery中的闭包
复制代码代码如下:
varcon=$("div#con");
setTimeout(function(){
con.css({background:"gray"});
},2000);


上边的代码使用了jQuery的选择器,找到id为con的div元素,注册计时器,当两秒中之后,将该div的背景色设置为灰色。这个代码片段的神奇之处在于,在调用了setTimeout函数之后,con依旧被保持在函数内部,当两秒钟之后,id为con的div元素的背景色确实得到了改变。应该注意的是,setTimeout在调用之后已经返回了,但是con没有被释放,这是因为con引用了全局作用域里的变量con。
使用闭包可以使我们的代码更加简洁,关于闭包的更详细论述可以在参考信息中找到。由于闭包的特殊性,在使用闭包时一定要小心,我们再来看一个容易令人困惑的例子:

清单10.错误的使用闭包
复制代码代码如下:
varoutter=[];
functionclouseTest(){
vararray=["one","two","three","four"];
for(vari=0;i<array.length;i++){
varx={};
x.no=i;
x.text=array[i];
x.invoke=function(){
print(i);
}
outter.push(x);
}
}


上边的代码片段很简单,将多个这样的JavaScript对象存入outter数组:

清单11.匿名对象
复制代码代码如下:
{
no:Number,
text:String,
invoke:function(){
//打印自己的no字段
}
}

我们来运行这段代码:

清单12.错误的结果
复制代码代码如下:
clouseTest();//调用这个函数,向outter数组中添加对象
for(vari=0,len=outter.length;i<len;i++){
outter[i].invoke();
}

出乎意料的是,这段代码将打印:
4
4
4
4

而不是1,2,3,4这样的序列。让我们来看看发生了什么事,每一个内部变量x都填写了自己的no,text,invoke字段,但是invoke却总是打印最后一个i。原来,我们为invoke注册的函数为:

清单13.错误的原因
复制代码代码如下:
functioninvoke(){
print(i);
}

每一个invoke均是如此,当调用outter[i].invoke时,i的值才会被去到,由于i是闭包中的局部变量,for循环最后退出时的值为4,因此调用outter中的每个元素都会得到4。因此,我们需要对这个函数进行一些改造:

清单14.正确的使用闭包
复制代码代码如下:
varoutter=[];
functionclouseTest2(){
vararray=["one","two","three","four"];
for(vari=0;i<array.length;i++){
varx={};
x.no=i;
x.text=array[i];
x.invoke=function(no){
return
function(){
print(no);
}
}(i);
outter.push(x);
}
}

通过将函数柯里化,我们这次为outter的每个元素注册的其实是这样的函数:
复制代码代码如下:
//x==0
x.invoke=function(){print(0);}
//x==1
x.invoke=function(){print(1);}
//x==2
x.invoke=function(){print(2);}
//x==3
x.invoke=function(){print(3);}


这样,就可以得到正确的结果了。

实际应用中的例子
好了,理论知识已经够多了,我们下面来看看现实世界中的JavaScript函数式编程。有很多人为使JavaScript具有面向对象风格而做出了很多努力(JavaScript本身具有可编程性),事实上,面向对象并非必须,使用函数式编程或者两者混合使用可以使代码更加优美,简洁。
jQuery是一个非常优秀JavaScript/Ajax框架,小巧,灵活,具有插件机制,事实上,jQuery的插件非常丰富,从表达验证,客户端图像处理,UI,动画等等。而jQuery最大的特点正如其宣称的那样,改变了人们编写JavaScript代码的风格。
优雅的jQuery
有经验的前端开发工程师会发现,平时做的最多的工作有一定的模式:选择一些DOM元素,然后将一些规则作用在这些元素上,比如修改样式表,注册事件处理器等。因此jQuery实现了完美的CSS选择器,并提供跨浏览器的支持:


清单15.jQuery选择器
复制代码代码如下:
varcons=$("div.note");//找出所有具有note类的div
varcon=$("div#con");//找出id为con的div元素
varlinks=$("a");//找出页面上所有的链接元素


当然,jQuery的选择器规则非常丰富,这里要说的是:用jQuery选择器选择出来的jQuery对象本质上是一个List,正如LISP语言那样,所有的函数都是基于List的。
有了这个List,我们可以做这样的动作:

清单16.jQuery操作jQuery对象(List)
复制代码代码如下:
cons.each(function(index){
$(this).click(function(){
//dosomethingwiththenode
});
});


想当与对cons这个List中的所有元素使用map(还记得我们前面提到的map吗?),操作结果仍然为一个List。我们可以任意的扩大/缩小这个列表,比如:

清单17.扩大/缩小jQuery集合
复制代码代码如下:
cons.find("span.title");//在div.note中进行更细的筛选
cons.add("div.warn");//将div.note和div.warn合并起来
cons.slice(0,5);//获取cons的一个子集


现在我们来看一个小例子,假设有这样一个页面:

清单18.页面的HTML结构
复制代码代码如下:
<divclass="note">
<spanclass="title">Hello,world</span>
</div>
<divclass="note">
<spanclass="title">345</span>
</div>
<divclass="note">
<spanclass="title">Hello,world</span>
</div>
<divclass="note">
<spanclass="title">67</span>
</div>
<divclass="note">
<spanclass="title">483</span>
</div>


效果如下:

图1.过滤之前的效果

我们通过jQuery对包装集进行一次过滤,jQuery的过滤函数可以使得选择出来的列表对象只保留符合条件的,在这个例子中,我们保留这样的div,当且仅当这个div中包含一个类名为title的span,并且这个span的内容为数字:

清单19.过滤集合
复制代码代码如下:
varcons=$("div.note").hide();//选择note类的div,并隐藏
cons.filter(function(){
return$(this).find("span.title").html().match(/^\d+$/);
}).show();


效果如下图所示:

图2.过滤之后的效果

我们再来看看jQuery中对数组的操作(本质上来讲,JavaScript中的数组跟List是很类似的),比如我们在前面的例子中提到的map函数,过滤器等:

清单20.jQuery对数组的函数式操作
复制代码代码如下:
varmapped=$.map([1,2,3,4,5,6,7,8,9,10],
function(n){
returnn+1;
});
vargreped=$.grep([1,2,3,4,5,6,7,8,9,10],
function(n){
returnn%2==0;
});


mapped将被赋值为:
[2,3,4,5,6,7,8,9,10,11]

而greped则为:
[2,4,6,8,10]

我们再来看一个更接近实际的例子:

清单21.一个页面刷新的例子
复制代码代码如下:
functionupdate(item){
return
function(text){
$("div#"+item).html(text);
}
}
functionrefresh(url,callback){
varparams={
type:"echo",
data:""
};
$.ajax({
type:"post",
url:url,
cache:false,
async:true,
dataType:"json",
data:params,
success:function(data,status){
callback(data);
},
error:function(err){
alert("error:"+err);
}
});
}
refresh("action.do/op=1",update("content1"));
refresh("action.do/op=2",update("content2"));
refresh("action.do/op=3",update("content3"));

首先声明一个柯里化的函数update,这个函数会将传入的参数作为选择器的id,并更新这个div的内容(innerHTML)。然后声明一个函数refresh,refresh接受两个参数,第一个参数为服务器端的url,第二个参数为一个回调函数,当服务器端成功返回时,调用该函数。
然后我们陆续调用三次refresh,每次的url和id都不同,这样可以将content1,content2,conetent3的内容通过异步方式更新。这种模式在实际的编程中相当有效,因为关于如何与服务器通信,以及如果选取页面内容的部分被很好的抽象成函数,现在我们需要做的就是将url和id传递给refresh,即可完成需要的动作。函数式编程在很大程度上降低了这个过程的复杂性,这正是我们选择使用该思想的最终原因。
结束语
实际的应用中,不会囿于函数式或者面向对象,通常是两者混合使用,事实上,很多主流的面向对象语言都在不断的完善自己,比如加入一些函数式编程语言的特征等,JavaScript中,这两者得到了良好的结合,代码不但可以非常简单,优美,而且更易于调试。
文中仅仅提到jQuery特征的一小部分,如果感兴趣,则可以在参考资料中找到更多的链接,jQuery非常的流行,因此你可以找到很多论述如何使用它的文章。
参考资料

jQuery官方网站的地址,可以下载到最新的jQuery库。

JavaScript中的闭包:一篇优秀的关于JavaScript闭包的论述。

文中提到的 LISP之根源的译文,该文详细的描述了LISP的其中基本原语,很好的解释了LISP的 可编程性

函数式编程的基本概念:一篇关于JavaScript函数式编程的基本概念的文章。

JavaScript框架比较”:在本文中,您将了解如何通过JavaScript框架更轻松、更快速地创建具有高度交互性和响应性的Web站点和Web应用程序。

JavaScript开发工具包 ”:本专题为您收集了一些和目前业界比较流行的JavaScript开发工具包相关的资源,从初级的入门介绍到高级的使用以及和其他开发语言、软件集成的内容。

developerWorks 技术活动网络广播:随时关注developerWorks技术活动和网络广播。 

developerWorksWebdevelopment专区:通过专门关于Web技术的文章和教程,扩展您在网站开发方面的技能。

developerWorksAjax资源中心:这是有关Ajax编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki和新闻。任何Ajax的新信息都能在这里找到。

developerWorksWeb2.0资源中心,这是有关Web2.0相关信息的一站式中心,包括大量Web2.0技术文章、教程、下载和相关技术资源。您还可以通过 Web2.0新手入门 栏目,迅速了解Web2.0的相关概念。