`

JavaScript 中的函数式编程实践

阅读更多

基础知识

函数式编程简介

说到函数式编程,人们的第一印象往往是其学院派,晦涩难懂,大概只有那些蓬头散发,不修边幅,甚至有些神经质的大学教授们才会用的编程方式。 这可能在历史上的某个阶段的确如此,但是近来函数式编程已经在实际应用中发挥着巨大作用了,而更有越来越多的语言不断的加入诸如 闭包匿 名函数 等的支持,从某种程度上来讲,函数式编程正在逐步“同化”命令式编程。

函数式编程思想的源头可以追溯到 20 世纪 30 年代,数学家阿隆左 . 丘奇在进行一项关于问题的可计算性的研究,也就是后来的 lambda 演算。lambda 演算的本质为 一切皆函数 ,函数可以作为另外一个函数的输出或者 / 和输入,一系列的函数使用最终会形成一个表达式链,这个表达式链可以最终求得一个值,而这个过程,即为计算的本质。

然而,这种思想在当时的硬件基础上很难实现,历史最终选择了同丘奇的 lambda 理论平行的另一种数学理论:图灵机作为计算理论,而采取另一位科学家冯 . 诺依曼的计算机结构,并最终被实现为硬件。由于第一台计算机即为冯 . 诺依曼的程序存储结构,因此运行在此平台的程序也继承了这种基因,程序设计语言如 C/Pascal 等都在一定程度上依赖于此体系。

到了 20 世纪 50 年代,一位 MIT 的教授 John McCarthy 在冯 . 诺依曼体系的机器上成功的实现了 lambda 理论,取名为 LISP(LISt Processor), 至此函数式编程语言便开始活跃于计算机科学领域。

函数式编程语言特性

在函数式编程语言中,函数是第一类的对象,也就是说,函数 依赖于任何其他的对象而可以独立存在,而在面向对象的语言 中,函数 ( 方法 ) 是依附于对象的,属于对象的一部分。这一点 j 决定了函数在函数式语言中的一些特别的性质,比如作为传出 / 传入参数,作为一个普通的变量等。

区别于命令式编程语言,函数式编程语言具有一些专用的概念,我们分别进行讨论:

匿名函数

在函数式编程语言中,函数是可以没有名字的,匿名函数通常表示:“可以完成某件事的一块代码”。这种表达在很多场合是有用的,因为我们有时需 要用函数完成某件事,但是这个函数可能只是临时性的,那就没有理由专门为其生成一个顶层的函数对象。比如:


清单 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; 
 }); 

 print(mapped); 

运行这段代码,将会打印:

 2,4,6,8,9// 对数组 [1,3,5,7,8] 中每一个元素加 1 

 

注意 map 函数的调用,map 的第二个参数为一个函数,这个函数对 map 的第一个参数 ( 数组 ) 中的每一个都有作用,但是对于 map 之外的代码可能没有任何意义,因此,我们无需为其专门定义一个函数,匿名函数已经足够。

柯里化

柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。 这句话有点绕口,我们可以通过例子来帮助理解:


清单 2. 柯里化函数

				
function adder(num){ 
  return 
				 function (x){ 
  return num + x; 
 } 
 } 

  var add5 = adder(5); 
  var add6 = 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. 高阶函数

				
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; 
 }); 

 print(mapped); 

  var mapped2 = 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 复兴时得到了彻底的扭转,Google Map,Gmail 等 Ajax 应用的出现使人们惊叹:原来 JavaScript 还可以做这样的事!很快,大量优秀的 JavaScript/Ajax 框架不断出现,比如 Dojo,Prototype,jQuery,ExtJS 等等。这些代码在给页面带来绚丽的效果的同时,也让开发者看到函数式语言代码的优雅。

函数式编程风格

在 JavaScript 中,函数本身为一种特殊对象,属于顶层对象,不依赖于任何其他的对象而存在,因此可以将函数作为传出 / 传入参数,可以存储在变量中,以及一切其他对象可以做的事情 ( 因为函数就是对象 )。

JavaScript 被称为有着 C 语法的 LISP,LISP 代码的一个显著的特点是大量的括号以及前置的函数名,比如:


清单 4. LISP 中的加法

				
 (+ 1 3 4 5 6 7) 

 

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


清单 5. JavaScript 中的求和

				
function sum(){ 
  var res = 0; 
  for ( var i = 0, len = arguments.length; i < len; i++){ 
 res += parseInt(arguments[i]); 
	 } 
  return res; 
 } 

 print(sum(1,2,3)); 
 print(sum(1,2,3,4,6,7,8)); 

 

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

 6 
 31 

 

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


清单 6. 一些简单的函数抽象

				
  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; } 

 

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


清单 7. 函数式编程风格

				
 // 修改之前的代码
  function factorial(n){ 
  if (n == 1){ 
  return 1; 
 } else { 
  return factorial(n - 1) * n; 
	 } 
 } 

 // 更接近“函数式”编程风格的代码
  function factorial(n){ 
     if (equal(n, 1)){ 
         return 1; 
    } else { 
         return mul(n, factorial(dec(n))); 
    } 
 } 

 

闭包及其使用

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


清单 8. 一个闭包的例子

				
function outter(){ 
  var n = 0; 
  return 
				 function (){ 
  return n++; 
 } 
 } 

  var o1 = outter(); 
 o1();//n == 0 
 o1();//n == 1 
 o1();//n == 2 
  var o2 = outter(); 
 o2();//n == 0 
 o2();//n == 1 

 

匿名函数 function(){return n++;} 中包含对 outter 的局部变量 n 的引用,因此当 outter 返回时,n 的值被保留 ( 不会被垃圾回收机制回收 ),持续调用 o1(),将会改变 n 的值。而 o2 的值并不会随着 o1() 被调用而改变,第一次调用 o2 会得到 n==0 的结果,用面向对象的术语来说,就是 o1 和 o2 为不同的 实例 ,互不干 涉。

总的来说,闭包很简单,不是吗?但是,闭包可以带来很多好处,比如我们在 Web 开发中经常用到的:


清单 9. jQuery 中的闭包

 var con = $("div#con"); 
 setTimeout( function (){ 
 con.css({background:"gray"}); 
 }, 2000); 

 

上边的代码使用了 jQuery 的选择器,找到 id 为 con 的 div 元素,注册计时器,当两秒中之后,将该 div 的背景色设置为灰色。这个代码片段的神奇之处在于,在调用了 setTimeout 函数之后,con 依旧被保持在函数内部,当两秒钟之后,id 为 con 的 div 元素的背景色确实得到了改变。应该注意的是,setTimeout 在调用之后已经返回了,但是 con 没有被释放,这是因为 con 引用了全局作用域里的变量 con。

使用闭包可以使我们的代码更加简洁,关于闭包的更详细论述可以在参考信息中找到。由于闭包的特殊性,在使用闭包时一定要小心,我们再来看一个 容易令人困惑的例子:


清单 10. 错误的使用闭包

				
  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 (){ 
 print(i); 
		 } 
		 outter.push(x); 
	 } 
 } 

 

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


清单 11. 匿名对象

				
 { 
 no : Number, 
 text : String, 
 invoke :  function (){ 
 // 打印自己的 no 字段
	 } 
 } 

 

我们来运行这段代码:


清单 12. 错误的结果

				
 clouseTest();// 调用这个函数,向 outter 数组中添加对象
 for ( var i = 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. 错误的原因

				
function invoke(){ 
 print(i); 
 } 

 

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


清单 14. 正确的使用闭包

 var outter = []; 
 function clouseTest2(){ 
  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 (){ 
 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 选择器

				
  var cons = $("div.note");// 找出所有具有 note 类的 div 
  var con = $("div#con");// 找出 id 为 con 的 div 元素
  var links = $("a");// 找出页面上所有的链接元素

 

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

有了这个 List,我们可以做这样的动作:


清单 16. jQuery 操作 jQuery 对象 (List)

				
 cons.each( function (index){ 
 $( this ).click( function (){ 
 //do something with the node 
	 }); 
 }); 

 

想当与对 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 结构

				
 <div class="note"> 
 <span class="title">Hello, world</span> 
 </div> 
 <div class="note"> 
 <span class="title">345</span> 
 </div> 
 <div class="note"> 
 <span class="title">Hello, world</span> 
 </div> 
 <div class="note"> 
 <span class="title">67</span> 
 </div> 
 <div class="note"> 
 <span class="title">483</span> 
 </div> 

 

效果如下:


图 1. 过滤之前的效果
图 1. 过滤之前的效果

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


清单 19. 过滤集合

				
 var cons = $("div.note").hide();// 选择 note 类的 div, 并隐藏
 cons.filter( function (){ 
  return $( this ).find("span.title").html().match(/^\d+$/); 
 }).show(); 

 

效果如下图所示:


图 2. 过滤之后的效果
图 2. 过滤之后的效果

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


清单 20. jQuery 对数组的函数式操作

				
 var mapped = $.map([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 
  function (n){ 
  return n + 1; 
 }); 
  var greped = $.grep([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 
  function (n){ 
  return n % 2 == 0; 
 }); 

 

mapped 将被赋值为 :

 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] 

 

而 greped 则为:

 [2, 4, 6, 8, 10] 

 

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


清单 21. 一个页面刷新的例子

				
function update(item){ 
  return 
				 function (text){ 
 $("div#"+item).html(text); 
	 } 
 } 
 function refresh(url, callback){ 
  var params = { 
 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 非常的流行,因此你可以找到很多论述如何使用它的文章。

<!-- CMA ID: 497588 --> <!-- Site ID: 10 --> <!-- XSLT stylesheet used to transform this file: dw-article-6.0-beta.xsl -->
分享到:
评论

相关推荐

    javascript函数式编程

    JavaScript函数式编程是利用JavaScript语言编写函数式风格代码的一种编程范式。函数式编程强调使用纯函数、避免副作用、函数的不可变性以及利用高阶函数等概念。通过阅读《JavaScript函数式编程指南》,读者可以了解...

    JavaScript函数式编程.pdf

    不过,由于【标题】中提供了文档的名称——"JavaScript函数式编程.pdf",我可以根据这个名称扩展出关于JavaScript函数式编程的知识点。 JavaScript函数式编程的知识点非常丰富,涉及很多方面的内容,下面将详细介绍...

    javascript指南和函数式编程

    而《JavaScript函数式.zip》可能是一份关于JavaScript函数式编程的资料集合,函数式编程是一种编程范式,强调使用函数和避免改变状态。其中可能涵盖以下知识点: 1. **纯函数**:理解纯函数的定义,即给定相同的...

    JavaScript ES6函数式编程入门经典

    JavaScript支持函数式编程,并允许开发者编写精心设计的代码。  主要内容  ●掌握函数式编程的概念  ●清楚函数在JavaScript中的地位  ●理解真实的函数式类库,并创建一个模拟underscore.js的函数式类库 ...

    JS 函数式编程指南 PDF

    这本指南不仅涵盖了函数式编程的基础理论,还详细介绍了如何在实际开发中应用这些理论,使之成为提高编程实践水平和深化编程理解的有力工具。通过学习和应用这些函数式编程技巧,开发者可以更好地理解和运用...

    一本关于JavaScript中函数式编程的书

    JavaScript中的函数式编程是一种强大的编程范式,它源自数学中的函数理论,强调将计算视为函数的求值,而不是状态的变化或指令的序列。在JavaScript中,函数式编程可以帮助我们写出更简洁、可读性强且易于测试的代码...

    JavaScript ES6函数式编程入门经典_javascript_tall7cj_

    JavaScript ES6函数式编程入门经典是一本针对JavaScript开发者,尤其是初学者的教程,旨在帮助他们掌握ES6(ECMAScript 2015)的新特性,并深入理解函数式编程的概念和实践。函数式编程是一种编程范式,它强调将计算...

    javascript 函数式编程

    JavaScript 函数式编程是一种编程范式,它将函数视为第一类公民,允许它们作为其他函数的参数、返回结果,甚至...通过学习和实践函数式编程,开发者可以更好地驾驭JavaScript,写出更高效、更易于理解和维护的代码。

    javascript函数式编程 underscore.js

    JavaScript函数式编程是一种编程范式,它强调使用函数来组织代码,将计算视为一系列惰性求值的操作。Underscore.js是一个轻量级的JavaScript实用库,它为开发者提供了大量函数式编程工具,使得在JavaScript中实践...

    JavaScript的函数式编程基础指南

    JavaScript的函数式编程基础指南旨在揭示JavaScript中函数式编程的概念和实践,尽管JavaScript通常被认为是一个面向对象的语言,但它并不依赖传统的类结构。函数式编程在JavaScript中的核心在于将函数视为一等公民,...

    函数式编程中文版.pdf

    JavaScript的灵活性使得开发者可以在不牺牲易用性的前提下尝试和采用函数式编程的实践。 学习函数式编程的最大障碍通常是在于语言本身。虽然命令式编程在JavaScript中占主导地位,但函数式编程为JavaScript提供了一...

    JavaScript 函数式编程 (英文版)

    下面将详细讨论JavaScript函数式编程的核心概念、实践方法以及它在实际开发中的应用。 1. **纯函数**:纯函数是给定相同的输入时,始终返回相同输出,并且不会对外部环境产生任何影响的函数。在JavaScript中,我们...

    用函数式编程技术编写优美的 JavaScript

    JavaScript本身具有一些内置的功能,如`map`、`reduce`等,这些都使得函数式编程风格在JavaScript中变得越来越流行。通过使用这些功能,开发者可以更容易地处理数据结构,编写出更加简洁、优雅的代码。 #### 重要...

    《JavaScript语言精髓与编程实践》精选版

    《JavaScript语言精髓与编程实践》这本书,最初的名字是叫《动态函数式语言精髓与编程实践》,这是作者写本书的原意。确切地说,作者并非是想讨论JavaScript作为一种语言工具的用法或特性,更多地是希望用一种简洁的...

    JavaScript函数式编程

    通过学习和实践这些JavaScript函数式编程的概念,开发者可以编写出更加优雅、高效和易于维护的代码,同时降低bug出现的可能性。本电子教程将详细解释每一个概念,并提供丰富的示例和练习,帮助读者掌握JavaScript...

    函数式编程及实例

    在JavaScript中,函数式编程的一些实践包括使用函数组合库如FunkierJS、使用纯函数处理数据,以及使用函数式工具如Redux来管理应用程序的状态。 总的来说,函数式编程提供了一种不同的思考问题和解决问题的方式,它...

    js设计模式详解和 函数式编程PDF

    JavaScript设计模式详解与函数式编程是开发者提升代码质量和可维护性的重要...通过阅读《JavaScript设计模式详解》和《函数式编程PDF》这样的资源,可以系统学习和实践这两种编程理念,从而在实际工作中更好地应用。

    663752 JavaScript ES6函数式编程入门经典.zip

    JavaScript ES6函数式编程是一种强大的编程范式,它在现代前端开发中占据着重要的地位。函数式编程强调将计算视为函数的求值,而非状态的改变或命令的执行,这为编写可读性强、可维护性高的代码提供了可能。本资料...

Global site tag (gtag.js) - Google Analytics