JavaScript 中的函數式編程實踐

基礎知識css

函數式編程簡介html

說到函數式編程,人們的第一印象每每是其學院派,晦澀難懂,大概只有那些蓬頭散發,不修邊幅,甚至有些神經質的大學教授們纔會用的編程方式。這可能在歷史上的某個階段的確如此,可是近來函數式編程已經在實際應用中發揮着巨大做用了,而更有愈來愈多的語言不斷的加入諸如 閉包匿名函數等的支持,從某種程度上來說,函數式編程正在逐步「同化」命令式編程。前端

函數式編程思想的源頭能夠追溯到 20 世紀 30 年代,數學家阿隆左 . 丘奇在進行一項關於問題的可計算性的研究,也就是後來的 lambda 演算。lambda 演算的本質爲 一切皆函數,函數能夠做爲另一個函數的輸出或者 / 和輸入,一系列的函數使用最終會造成一個表達式鏈,這個表達式鏈能夠最終求得一個值,而這個過程,即爲計算的本質。node

然而,這種思想在當時的硬件基礎上很難實現,歷史最終選擇了同丘奇的 lambda 理論平行的另外一種數學理論:圖靈機做爲計算理論,而採起另外一位科學家馮 . 諾依曼的計算機結構,並最終被實現爲硬件。因爲第一臺計算機即爲馮 . 諾依曼的程序存儲結構,所以運行在此平臺的程序也繼承了這種基因,程序設計語言如 C/Pascal 等都在必定程度上依賴於此體系。jquery

到了 20 世紀 50 年代,一位 MIT 的教授 John McCarthy 在馮 . 諾依曼體系的機器上成功的實現了 lambda 理論,取名爲 LISP(LISt Processor), 至此函數式編程語言便開始活躍於計算機科學領域。web

函數式編程語言特性ajax

在函數式編程語言中,函數是第一類的對象,也就是說,函數 依賴於任何其餘的對象而能夠獨立存在,而在面向對象的語言中,函數 ( 方法 ) 是依附於對象的,屬於對象的一部分。這一點 j 決定了函數在函數式語言中的一些特別的性質,好比做爲傳出 / 傳入參數,做爲一個普通的變量等。shell

區別於命令式編程語言,函數式編程語言具備一些專用的概念,咱們分別進行討論:編程

匿名函數json

在函數式編程語言中,函數是能夠沒有名字的,匿名函數一般表示:「能夠完成某件事的一塊代碼」。這種表達在不少場合是有用的,由於咱們有時須要用函數完成某件事,可是這個函數可能只是臨時性的,那就沒有理由專門爲其生成一個頂層的函數對象。好比:


清單 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 很是的流行,所以你能夠找到不少論述如何使用它的文章。


參考資料

  • jQuery官方網站的地址,能夠下載到最新的 jQuery 庫。

  • JavaScript 中的閉包:一篇優秀的關於 JavaScript 閉包的論述。

  • 文中提到的 LISP 之根源的譯文,該文詳細的描述了 LISP 的其中基本原語,很好的解釋了 LISP 的 可編程性

  • 函數式編程的基本概念:一篇關於 JavaScript 函數式編程的基本概念的文章。

  • JavaScript 框架比較」:在本文中,您將瞭解如何經過 JavaScript 框架更輕鬆、更快速地建立具備高度交互性和響應性的 Web 站點和 Web 應用程序。

  • JavaScript 開發工具包 」:本專題爲您收集了一些和目前業界比較流行的 JavaScript 開發工具包相關的資源,從初級的入門介紹到高級的使用以及和其餘開發語言、軟件集成的內容。

  • developerWorks 技術活動網絡廣播:隨時關注 developerWorks 技術活動和網絡廣播。 

  • developerWorks Web development 專區:經過專門關於 Web 技術的文章和教程,擴展您在網站開發方面的技能。

  • developerWorks Ajax 資源中心:這是有關 Ajax 編程模型信息的一站式中心,包括不少文檔、教程、論壇、blog、wiki 和新聞。任何 Ajax 的新信息都能在這裏找到。

  • developerWorks Web 2.0 資源中心,這是有關 Web 2.0 相關信息的一站式中心,包括大量 Web 2.0 技術文章、教程、下載和相關技術資源。您還能夠經過 Web 2.0 新手入門 欄目,迅速瞭解 Web 2.0 的相關概念。
相關文章
相關標籤/搜索