JavaScript 中的函數式編程實踐

jopen 13年前發布 | 45K 次閱讀 JavaScript開發 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 </pre></td> </tr> </tbody> </table>

注意 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)); </pre></td> </tr> </tbody> </table>

結果為:

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); </pre></td> </tr> </tbody> </table>

將會打印如下結果:

</tr> </tbody> </table>

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 中的加法

 2,4,6,8,9 
 (one),(two),(three),(four)// 為數組中的每個字符串加上括號

</tr> </tbody> </table>

加號在 LISP 中為一個函數,這條表達式的意思為將加號后邊的所有數字加起來,并將值返回,JavaScript 可以定義同樣的求和函數:


清單 5. JavaScript 中的求和

               
 (+ 1 3 4 5 6 7) 


  • 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)); </pre></td> </tr> </tbody> </table>

    運行此段代碼,得到如下結果:

    </tr> </tbody> </table>

    如果要完全模擬函數式編碼的風格,我們可以定義一些諸如:


    清單 6. 一些簡單的函數抽象

     6 
     31 

    </tr> </tbody> </table>

    這樣的小函數以及謂詞,那樣我們寫出的代碼就更容易被有函數式編程經驗的人所接受:


    清單 7. 函數式編程風格

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


    // 修改之前的代碼 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))); } } </pre></td> </tr> </tbody> </table>

    閉包及其使用

    閉包是一個很有趣的主題,當在一個函數 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 </pre></td> </tr> </tbody> </table>

    匿名函數 function(){return n++;} 中包含對 outter 的局部變量 n 的引用,因此當 outter 返回時,n 的值被保留 ( 不會被垃圾回收機制回收 ),持續調用 o1(),將會改變 n 的值。而 o2 的值并不會隨著 o1() 被調用而改變,第一次調用 o2 會得到 n==0 的結果,用面向對象的術語來說,就是 o1 和 o2 為不同的 實例,互不干涉。

    總的來說,閉包很簡單,不是嗎?但是,閉包可以帶來很多好處,比如我們在 Web 開發中經常用到的:


    清單 9. jQuery 中的閉包

    </tr> </tbody> </table>

    上邊的代碼使用了 jQuery 的選擇器,找到 id 為 con 的 div 元素,注冊計時器,當兩秒中之后,將該 div 的背景色設置為灰色。這個代碼片段的神奇之處在于,在調用了 setTimeout 函數之后,con 依舊被保持在函數內部,當兩秒鐘之后,id 為 con 的 div 元素的背景色確實得到了改變。應該注意的是,setTimeout 在調用之后已經返回了,但是 con 沒有被釋放,這是因為 con 引用了全局作用域里的變量 con。

    使用閉包可以使我們的代碼更加簡潔,關于閉包的更詳細論述可以在參考信息中找到。由于閉包的特殊性,在使用閉包時一定要小心,我們再來看一個容易令人困惑的例子:


    清單 10. 錯誤的使用閉包

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

    </tr> </tbody> </table>

    上邊的代碼片段很簡單,將多個這樣的 JavaScript 對象存入 outter 數組:


    清單 11. 匿名對象

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

    </tr> </tbody> </table>

    我們來運行這段代碼:


    清單 12. 錯誤的結果

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

    </tr> </tbody> </table>

    出乎意料的是,這段代碼將打印:

                   
     clouseTest();// 調用這個函數,向 outter 數組中添加對象
     for ( var i = 0, len = outter.length; i < len; i++){ 
         outter[i].invoke(); 
     } 

    </tr> </tbody> </table>

    而不是 1,2,3,4 這樣的序列。讓我們來看看發生了什么事,每一個內部變量 x 都填寫了自己的 no,text,invoke 字段,但是 invoke 卻總是打印最后一個 i。原來,我們為 invoke 注冊的函數為:


    清單 13. 錯誤的原因

     4 
     4 
     4 
     4 

    </tr> </tbody> </table>

    每一個 invoke 均是如此,當調用 outter[i].invoke 時,i 的值才會被去到,由于 i 是閉包中的局部變量,for 循環最后退出時的值為 4,因此調用 outter 中的每個元素都會得到 4。因此,我們需要對這個函數進行一些改造:


    清單 14. 正確的使用閉包

                   
    function invoke(){ 
     print(i); 
     } 

    </tr> </tbody> </table>

    通過將函數 柯里化,我們這次為 outter 的每個元素注冊的其實是這樣的函數:

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

    </tr> </tbody> </table>

    這樣,就可以得到正確的結果了。

     

    實際應用中的例子

    好了,理論知識已經夠多了,我們下面來看看現實世界中的 JavaScript 函數式編程。有很多人為使 JavaScript 具有面向對象風格而做出了很多努力 (JavaScript 本身具有 可編程性),事實上,面向對象并非必須,使用函數式編程或者兩者混合使用可以使代碼更加優美,簡潔。

    jQuery 是一個非常優秀 JavaScript/Ajax 框架,小巧,靈活,具有插件機制,事實上,jQuery 的插件非常豐富,從表達驗證,客戶端圖像處理,UI,動畫等等。而 jQuery 最大的特點正如其宣稱的那樣,改變了人們編寫 JavaScript 代碼的風格。

    優雅的 jQuery

    有經驗的前端開發工程師會發現,平時做的最多的工作有一定的模式:選擇一些 DOM 元素,然后將一些規則作用在這些元素上,比如修改樣式表,注冊事件處理器等。因此 jQuery 實現了完美的 CSS 選擇器,并提供跨瀏覽器的支持:


    清單 15. jQuery 選擇器

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

    </tr> </tbody> </table>

    當然,jQuery 的選擇器規則非常豐富,這里要說的是:用 jQuery 選擇器選擇出來的 jQuery 對象本質上是一個 List,正如 LISP 語言那樣,所有的函數都是基于 List 的。

    有了這個 List,我們可以做這樣的動作:


    清單 16. jQuery 操作 jQuery 對象 (List)

                   
      var cons = $("div.note");// 找出所有具有 note 類的 div 
      var con = $("div#con");// 找出 id 為 con 的 div 元素
      var links = $("a");// 找出頁面上所有的鏈接元素

    </tr> </tbody> </table>

    想當與對 cons 這個 List中的所有元素使用 map( 還記得我們前面提到的 map 嗎? ),操作結果仍然為一個 List。我們可以任意的擴大 / 縮小這個列表,比如:


    清單 17. 擴大 / 縮小 jQuery 集合

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

    </tr> </tbody> </table>

    現在我們來看一個小例子,假設有這樣一個頁面:


    清單 18. 頁面的 HTML 結構

                   
     cons.find("span.title");// 在 div.note 中進行更細的篩選
     cons.add("div.warn");// 將 div.note 和 div.warn 合并起來
     cons.slice(0, 5);// 獲取 cons 的一個子集

    </tr> </tbody> </table>

    效果如下:


    圖 1. 過濾之前的效果
    圖 1. 過濾之前的效果 

    我們通過 jQuery 對包裝集進行一次過濾,jQuery 的過濾函數可以使得選擇出來的列表對象只保留符合條件的,在這個例子中,我們保留這樣的 div,當且僅當這個 div 中包含一個類名為 title 的 span,并且這個 span 的內容為數字:


    清單 19. 過濾集合

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

    </tr> </tbody> </table>

    效果如下圖所示:


    圖 2. 過濾之后的效果
    圖 2. 過濾之后的效果 

    我們再來看看 jQuery 中對數組的操作 ( 本質上來講,JavaScript 中的數組跟 List 是很類似的 ),比如我們在前面的例子中提到的 map 函數,過濾器等:


    清單 20. jQuery 對數組的函數式操作

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

    </tr> </tbody> </table>

    mapped 將被賦值為 :

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

    </tr> </tbody> </table>

    而 greped 則為:

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

    </tr> </tbody> </table>

    我們再來看一個更接近實際的例子:


    清單 21. 一個頁面刷新的例子

     [2, 4, 6, 8, 10] 


    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")); </pre></td> </tr> </tbody> </table>

    首先聲明一個柯里化的函數 update,這個函數會將傳入的參數作為選擇器的 id,并更新這個 div 的內容 (innerHTML)。然后聲明一個函數 refresh,refresh 接受兩個參數,第一個參數為服務器端的 url,第二個參數為一個回調函數,當服務器端成功返回時,調用該函數。

    然后我們陸續調用三次 refresh,每次的 url 和 id 都不同,這樣可以將 content1,content2,conetent3 的內容通過異步方式更新。這種模式在實際的編程中相當有效,因為關于如何與服務器通信,以及如果選取頁面內容的部分被很好的抽象成函數,現在我們需要做的就是將 url 和 id 傳遞給 refresh,即可完成需要的動作。函數式編程在很大程度上降低了這個過程的復雜性,這正是我們選擇使用該思想的最終原因。

     

    結束語

    實際的應用中,不會囿于函數式或者面向對象,通常是兩者混合使用,事實上,很多主流的面向對象語言都在不斷的完善自己,比如加入一些函數式編程語言的特征等,JavaScript 中,這兩者得到了良好的結合,代碼不但可以非常簡單,優美,而且更易于調試。

    文中僅僅提到 jQuery 特征的一小部分,如果感興趣,則可以在參考資料中找到更多的鏈接,jQuery 非常的流行,因此你可以找到很多論述如何使用它的文章。


    原文地址:http://www.ibm.com/developerworks/cn/web/1006_qiujt_jsfunctional/index.html

     本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
  • sesese色