Js高级函数式编程

来源:互联网 发布:企业it解决方案 编辑:程序博客网 时间:2024/05/18 17:00

函数式编程语言特性 
在函数式编程语言中,函数是第一类的对象,也就是说,函数 不依赖于任何其他的对象而可以独立存在,而在面向对象的语言中,函数 ( 方法 ) 是依附于对象的,属于对象的一部分。这一点 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); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

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));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

结果为:

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

将会打印如下结果: 
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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

如果要完全模拟函数式编码的风格,我们可以定义一些诸如: 
清单 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; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

清单 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)));     }  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

闭包及其使用 
闭包是一个很有趣的主题,当在一个函数 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

匿名函数 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);
  • 1
  • 2
  • 3
  • 4

上边的代码使用了 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);      }  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

 {  no : Number,  text : String,  invoke :  function (){  // 打印自己的 no 字段     }  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们来运行这段代码: 
清单 12. 错误的结果

 clouseTest();// 调用这个函数,向 outter 数组中添加对象 for ( var i = 0, len = outter.length; i < len; i++){      outter[i].invoke();  }
  • 1
  • 2
  • 3
  • 4

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




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

function invoke(){  print(i);  }
  • 1
  • 2
  • 3

每一个 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);      }   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

通过将函数 柯里化,我们这次为 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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

效果如下: 
图 1. 过滤之前的效果 
图 1. 过滤之前的效果 
我们通过 jQuery 对包装集进行一次过滤,jQuery 的过滤函数可以使得选择出来的列表对象只保留符合条件的,在这个例子中,我们保留这样的 div,当且仅当这个 div 中包含一个类名为 title 的 span,并且这个 span 的内容为数字: 
清单 19. 过滤集合 
var cons = (div.note).hide();//notediv,cons.filter(function()return$(this).find("span.title").html().match(/\d+$/);).show();2.2.jQuery(JavaScriptList)map20.jQueryvarmapped=.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"));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

首先声明一个柯里化的函数 update,这个函数会将传入的参数作为选择器的 id,并更新这个 div 的内容 (innerHTML)。然后声明一个函数 refresh,refresh 接受两个参数,第一个参数为服务器端的 url,第二个参数为一个回调函数,当服务器端成功返回时,调用该函数。 
然后我们陆续调用三次 refresh,每次的 url 和 id 都不同,这样可以将 content1,content2,conetent3 的内容通过异步方式更新。这种模式在实际的编程中相当有效,因为关于如何与服务器通信,以及如果选取页面内容的部分被很好的抽象成函数,现在我们需要做的就是将 url 和 id 传递给 refresh,即可完成需要的动作。函数式编程在很大程度上降低了这个过程的复杂性,这正是我们选择使用该思想的最终原因。

原创粉丝点击