《JavaScript函數式編程》讀書筆記

JavaScript是一門很神奇的語言,做爲一門現代化的語言,他有不少頗有特點的東西,這些東西,讓咱們看到了一個十分自由化的將來,你永遠都不知道,本身是否是掌握了這門奇葩的要命的語言。本文,可能沒有那麼多高深的編程技巧,有的更多的是,對編程自己的理解。由於,不知道本身有多白癡,因此,要記錄下來,等到本身不白癡的時候,就能緬懷當年那個白癡的本身了。javascript

什麼是函數式編程

所謂函數式編程,是把函數自己上升到一等公民的地位,進行編程構建。在書中,做者用了這麼一句話來形容函數式編程:php

函數式編程經過使用函數來將值轉換成抽象單元,接着用於構建軟件系統。html

額,那麼好,咱們先回憶一下什麼叫函數。java

函數

通常的,在一個變化過程當中,有兩個變量x、y,若是給定一個x值,相應的就肯定惟一的一個y,那麼就稱y是x的函數,其中x是自變量,y是因變量,x的取值範圍叫作這個函數的定義域,相應y的取值範圍叫作函數的值域。jquery

這是數學中的定義,簡單的說,函數就是從A到B的關係映射。在計算機中,咱們將多條語句組成的程序段(程序塊)叫作函數,一個函數自己應該有必定的意義。和數學定義至關的是,變量的生命週期所在的函數空間,爲變量的定義域。算法

面向函數的編程

所謂函數式編程,咱們又能夠叫作是面向函數的編程。所謂面向函數就是使用函數來做爲咱們分析和抽象程序的主要工具。編程

嗯,首先,咱們繼續來複習一下json

  • 什麼叫作面向過程。數組

「面向過程」(Procedure Oriented)是一種以過程爲中心的編程思想。「面向過程」也可稱之爲「面向記錄」編程思想,他們不支持豐富的「面向對象」特性(好比繼承、多態),而且它們不容許混合持久化狀態和域邏輯。promise

其實,說白了,就是想到什麼寫什麼。

  • 什麼叫作面向對象

按人們認識客觀世界的系統思惟方式,採用基於對象(實體)的概念創建模型,模擬客觀世界分析、設計、實現軟件的辦法。經過面向對象的理念使計算機軟件系統能與現實世界中的系統一一對應。

在面向對象中,咱們都知道對象是兩個很重要的概念。

咱們知道,所謂的類,其實就是:

具備相同特性(數據元素)和行爲(功能)的對象的抽象就是類。所以,對象的抽象是類,類的具體化就是對象,也能夠說類的實例是對象,類實際上就是一種數據類型。

而咱們所說的對象,其實就是:

對象是人們要進行研究的任何事物,從最簡單的整數到複雜的飛機等都可看做對象,它不只能表示具體的事物,還能表示抽象的規則、計劃或事件。

咱們不難發現,類和對象,其實都是從數據角度出發來思考和解決問題,以數據自己爲運算核心來抽象咱們的計算行爲。可是,不少時候,咱們會發現,其實咱們的運算行爲遠遠比數據自己要複雜,並且,咱們不少時候,其實並不能很好的去抽象一個對象。

個人數據老師曾經這樣教導咱們:

所謂程序,就是數據結構加算法。

若是說,面向對象是從數據結構的角度出發的話,面向函數的編程,就是從算法角度出發,也就是從行爲的角度出發。

爲何要用函數式編程

數據和行爲的關係

在計算機中,數據多數指的是存儲結構。行爲指的多數是計算操做。好比說這段代碼:

function say(){
        let a = 1 + 1; 
        console.log(a)
    }

這段代碼裏,做爲變量存在的asay,是咱們所熟知的數據,而function say()則是包含了整個說的行爲。
在面向對象的編程中,咱們習慣把對象做爲行爲的核心,也就是說,先有人,而後,人來執行一個動做。而,對象,其實就是某一種變量,亦或是某一種數據類型。而函數式編程中,則認爲數據只是行爲加工的產品。將行爲和數據分離。咱們來看一段代碼。

// man.php
    class Man {
        function __constructor($sexy){
            $this->sexy = $sexy;
        }
        public function sayHello($string){
            echo "I'm a ".$this->sexy.",I want to say:".$string;
        }
    }
    // male.php
    require_once 'man.php'
    $male = new Man("man");
    $male->sayHello("my name is homker");
    // I'm a man, I want to say: my name is homker

tips:
由於javascript自己是沒有類的概念的,爲了更好的說明問題,這裏使用了php來做爲範例語言,固然,你也可使用javascript面向對象的方式來從新實現這段代碼。就像這樣。

function Man(sexy){
       var self = this;
       self._sexy = sexy;
       self.sayHello = function(string){
           console.log("I'm a "+self._sexy+",I want to say:"+string);
       }
   }

   var male = new Man("man");
   male.sayHello('my name is homker');

這是一段很簡單的面向對象的代碼,咱們看看一樣的功能在函數式中要怎麼作。

function Man(sexy){
        return function(string){
            console.log("I'm a "+sexy+",I want to say:"+string);
        }
    }
    
    var sayHello = Man('man');
    sayHello('my name is homker');

咱們會發現,在函數式編程中,咱們去除掉了主語。你不知道這個動做是由誰發出的。相比於在面向對象編程中,數據是對象的屬性,在函數式編程中,咱們並不在意這個數據的內容是什麼,而更在意其變化。

額,固然,嚴格意義上來講,其實,這個sayHello的原型是Object,在瀏覽器端,追溯他的原型鏈,它是掛在window對象下面的。

專一於過程自己

在實際的開發過程當中,咱們有的時候很難抽象出一個對象來描述咱們到底要作什麼,或者說,咱們其實並不在意這堆數據裏面的內容是什麼,咱們要關心的,只是把數據通過加工,得出結果,僅此而已。至於這個數據,究竟是用來幹什麼的,咱們其實能夠並不用關心。

如何使用函數式編程

上面說的都是一些思惟上的東西,可能很稚嫩,但願各位大大們能指出其中的錯誤,切不可吝嗇言語。下面就來講說函數式編程的一些具體的東西。

一等公民

所謂一等公民,說的是函數自己能夠成爲代碼構成中的任意部分。具體的來講,函數能夠具備如下的特色:

  • 函數能夠存儲爲變量

  • 函數能夠成爲數組的一個元素

  • 函數能夠成爲對象的成員變量

  • 函數能夠在使用的時被直接建立

1 + (function(){ return 1 })(); //2
  • 函數能夠被做爲實參傳遞

  • 函數能夠被另外一個函數返回

  • 函數能夠返回另外一個函數

  • 函數能夠做爲形參

相信你們一看就懂了。

純函數 (Pure Function)

在函數式編程中,有一個很重要的概念是純函數。所謂純函數就是

純函數(Pure Function)與外界交換數據只有惟一渠道——參數和返回值。其在邏輯上沒有反作用

可預見性

簡單的說,就是你輸入什麼,就輸出什麼。輸入和輸出是可預見的。好比說像醬:

function add(x,y){
        x = _.isNumber(x)? x : 0;
        y = _.isNumber(y)? y : 0;
        return x+y;
    }
    
    add(1,2); // 3

這樣的一個函數,你輸入兩個變量,你能夠很肯定的,你獲得的必定是二者之和。與之相異的,在javascript編程中很容易出現的,好比說醬:

var x = 10;
    function add10(y){
        return y+x;
    }
    
    add10(1); // 11
    
    x = 11;
    
    add10(1); //12

對於這個函數而言,函數自己是不可控的,若是外部的x發生改變,函數的返回值也隨之會發生改變。那麼若是想避免,應該怎麼寫呢:

function add(x){
        return function(y){
            return x+y;
        }
    }
    
    var add10 = add(10);
    
    add10(1); //11

這個時候,將函數所需的變量閉包在函數體的內部,函數的運算是不依賴於外界的變量的,你輸入什麼,就必定會輸出什麼。

完整性

爲了實現函數的可控性,要保證,函數本省是完整的。函數的完整表如今,函數的運行不依賴於外界的環境變量。同時,函數的邏輯是完整的。好比說,醬:

<!DOCTYPE html>
    <html>
        <head>
            <title>a demo</title>
            <link herf="path/to/style" rel="stylesheet" />
            <script src="path/to/jq"></script>
        </head>
        <body>
            <div class=「container」>
                <span id="display"></span>
                <button id="getJson">獲取數據</button>
            </div>
            <script type="appliaction/javascript">
                ;(function(){
                    $('#getJson').addEventListener('click',function(){
                        $.get('path/to/json',function(json){
                            $('#display').text(json);
                        })
                    },false)
                })()
            </script>
        </body>
    </html>

上面是咱們常常寫的方式,固然啦,若是框架複雜一點,可能會多一點回調嵌套。可是,邏輯不出於此。可是呢,若是要函數完整,應該醬,額,我就寫重要的部分啦:

var getJson = function(url,params){
        return $.getJson(url,params);
    }
    
    var display = function(text){
        $('#display').text(text)
    }
    
    var getJsonClickHandle = function(){
        getJson('url',{}).done(display)
    }
    
    var init = function(){
        $('#getJson').click(getJsonClickHandle);
    }
    
    
    init();

這時候,咱們抽象了整個行爲。

點擊 -> 獲取數據 -> 顯示數據。

醬,咱們把每一個行爲轉換成了一個單獨的函數行爲。這樣的,每個函數都是單獨的行爲,能夠很好的擴展和複製到其餘地方。

同時,咱們也引出了一個純函數很重要的部分。

可測試

咱們發現,當函數功能變的單一的時候,咱們能夠很清晰的知道函數輸入什麼,輸出什麼的時候,咱們發現,這個代碼的可測試性,獲得了很大的提升。仍是用上面的兩段代碼,前者,根本不知道怎麼去寫測試,或者說,就是錯了,你也不知道哪裏錯的,由於,全部的邏輯被各類匿名函數包裹了,很難很快的定位到問題的所在,後者,則容易了不少。

可組合(compose)

當函數純化以後,有一個很鮮明的特色是,這個函數變的能夠組合了。咱們能夠像堆樂高積木同樣,把各個咱們要用的函數堆起來變成一個更大得函數體。好比說醬:

使用了underscore.js;

function checker(){
        var validators = _.toArray(arguments);
        return function(obj){
            return _.reducer(validators,function(err,check){
                if(check(obj)){
                    return errs;
                }else{
                    return _.chain(errs).push(check.message).value();
                }
            },[])
        }
    }
    
    function validator(message,fun){
        var f = function(){
            return fun.apply(fun,arguments);
        };
        f['message'] = message;
        return f;
    }
    
    function hasKeys(){
        var KEYS = _.toArray(arguments);
        
        var fun = function(obj){
            return _.every(KEYS,function(key){
                return _.has(obj,key);
            });
        };
        
        fun.message = KEYS.concat([",this key is valid"]).join(" ");
        return fun;
    }
    
    var checkCommand = checker(hasKeys('msg','type'));
    
    checkCommand({}); // msg type , this key is valid

checkCommand就是咱們最後組合出來的能夠進行校驗的大城堡了,並且這個城堡能夠定製化哦,甚至必要的時候,能夠動態定製化。

高階函數(high-order function)

高階函數是函數式編程中,很重要的部分,咱們先來看看它是怎麼定義的。做爲一個高階函數,他要知足如下定義:

  • 高階函數必定是一等公民

  • 以一個函數爲參數

  • 同時返回一個函數做爲函數的返回值

舉一個簡單的例子:

var aFunc = (function(callback){
        return function(){
            callback&&callback();
        }
    })(function(){ console.log("I am a high-order function") });
    
    aFunc();// I am a high-order function;

額,呵呵,這個例子比較無聊哈,咱們看個更有意思的例子:

function calc(x){
        return function(y){
            return function(method){
                method&&method(x)(y);
            }
        }
    }
    function add(x){
        return function(y){
            console.log(x+y);
        }
    }
    calc(1)(2)(add);//3

固然,你再無聊點,非要寫成這樣,也不是不能夠。

function calc(x){
        return function(method){
            return function(y){
                method&&method(x)(y);
            }
        }
    }
    calc(1)(add)(2);

其中的add方法是可自定義的。你能夠把它換成任何一個你想要的函數。

柯理化函數(curry)

柯理化函數,是函數編程中很重要的一個方法。嗯,咱們先來看看定義:

只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。

上文的add,calc都是柯理化的函數。
在平時的使用中,咱們常常如此使用之:

  • 接收一個函數

  • 返回一個只接收一個參數的函數

柯理化函數的定義是函數式編程的基礎,咱們經過返回一個閉包的方式來使得函數參數變的能夠捕捉,能夠傳遞,能夠保存。同時也使得,函數的行爲變的能夠分離,能夠組合。

柯理方向

嗯,咱們知道運算符是有方向的,函數組合的大函數也是同樣的。好比說,下面兩個函數就不同:

var leftdiv(x){
        return function(y){
            return x/y;
        }
    }
    
    var rightdiv(x){
        return function(y){
            return y/x;
        }
    }

部分引用

咱們說到柯理化的函數能夠保存參數,或者說成是保留運算場景。好比說咱們在上文舉的add函數:

var add10 = add(10);
    add10(1);//11

其中的add10就是部分引用,add10這個函數保留了上一次函數調用時的運算場景,當下一個參數進來的時候,它可以繼續運行,並給出結果。這樣的好處是什麼呢,咱們能夠實現核心運算的前置條件校驗。

好比說醬:

var add = function(x){
        if(!isNumber(x)) throw Error(' x must be a num');
        return function(y){
            if(!isNumber(y)) throw Error(' y must be a num');
            return function(){
                return x+y;
            }
        }
    }

咱們在每一次的調用的時候,咱們順便作了輸入參數的校驗,當最後函數執行的時候,咱們能夠確保,最後的函數執行是可靠的,也就是該函數是純的。

組合

上文在說純函數的時候,咱們就已經說到了組合了,這裏,咱們再強調的地方是組合函數的管道特性。就是把上一個函數的值做爲下一個函數的參數。
就像醬:

var compose = function(f,g){
        return function(x){
            return f(g(x));
        }
    }

基於流的編程

其實,對於函數式編程,咱們總結其技巧的時候,發現,其功能是圍繞於:

  • 用函數傳遞函數

  • 用函數構造函數

  • 用函數調用函數

而這三個綜合在一塊兒,使得函數式編程可以實現基於數據流或者控制流。

鏈式編程

這個咱們都很熟悉啦,jquery就是這樣乾的,經過返回一個自身來實現鏈式調用。就像醬:

$.prototype.next(){
        //do something
        return this;
    }
    
    $('li').next().next();

promise

這個其實單獨拿出來,寫一本書。因此這裏就不詳細說了。例子的話,上文也有舉getJson,這裏就不舉了。

鏈式編程和promise能更好的,讓咱們按照數據處理的階段去處理函數,在開始的進行參數校驗,在加工的時候,進行數據的加工,在最後的時候,進行函數的顯示。

總結

其實,這本翻來覆去的看了好幾遍,一直想作一個總結,可是並不能作的出來。由於,咱們很容易發現,在實際的操做過程當中,咱們或多或少的都使用了函數式編程的一部分,咱們或多或少的都在踐行函數式編程的理論,可是,若是說,咱們的代碼就是使用函數式編程的時候,咱們又會發現,咱們的代碼中,有很大一部分的邏輯,實在是沒辦法使用函數式編程進行處理。因此,後面有了響應式編程RXJs,經過訂閱和發佈模式來實現隊列化的事件調度和資源分配,可是在實際使用過程當中,要想很快的將代碼轉化成函數式編程,須要對行爲邏輯有很深入的理解和抽象,對步驟的分解,對函數的分解,對行爲的分解,這個纔是函數式編程中最難的部分,如何去思考你的數據發生了什麼變化,你的狀態發生了什麼變化,去管理你的數據和你的狀態。

相關文章
相關標籤/搜索