javascript基礎修煉(8)——指向FP世界的箭頭函數

一. 箭頭函數

箭頭函數是ES6語法中加入的新特性,而它也是許多開發者對ES6僅有的瞭解,每當面試裏被問到關於ES6裏添加了哪些新特性?」這種問題的時候,幾乎老是會拿箭頭函數來應付。箭頭函數,=>,沒有本身的this , arguments , super , new.target「書寫簡便,沒有this」在很長一段時間內涵蓋了大多數開發者對於箭頭函數的所有認知(固然也包括我本身),若是隻是爲了簡化書寫,把=>按照function關鍵字來解析就行了,何須要弄出一個跟普通函數特性不同的符號呢?答案就是:函數式編程(Functional Programming)javascript

若是你瞭解javascript這門語言就知道,它是沒有類這個東西的,ES6新加入的Class關鍵字,也不過是語法糖而已,咱們不斷被要求使用面向對象編程的思想來使用javascript,定義不少類,用複雜的原型鏈機制去模擬類,是由於更多的開發者可以習慣這種描述客觀世界的方式,《你不知道的javascript》中就明確指出原型鏈的機制其實只是實現了一種功能委託機制,即使不使用面向對象中的概念去描述它,這也是一種合乎邏輯的語言設計方案,並不會形成巨大的認知障礙。但須要明確的是,面向對象並非javascript惟一的使用方式。html

固然我也是接觸到【函數式編程】的思想後才意識到,我並非說【函數式編程】優於【面向對象】,每一種編程思想都有其適用的範圍,但它的確向我展現了另外一種對編程的認知方式,並且在流程控制的清晰度上,它的確比面向對象更棒,它甚至讓我開始以爲,這纔是javascript該有的打開方式。前端

若是你也曾覺得【函數式編程】就是「用箭頭函數把函數寫的精簡一些」,若是你也被各類複雜的this綁定弄的暈頭轉向,那麼就一塊兒來看看這個胖箭頭指向的新世界——Functional Programming吧!vue

二. 更貼近本能的思惟方式

假若有這樣一個題目:java

在傳統編程中,你的編碼過程大約是這樣:git

let resolveYX = (x) => 3*x*x + 2*x + 1;
let resolveZY = (y) => 4*y*y*y + 5*y*y + 6;
let resolveRZ = (z) => (2*z*z - 4)/3;
let y = resolveYX(2);
let z = resolveZY(y);
let result = resolveRZ(z);

咱們大多時候採用的方式是把程序的執行細節用程序語言描述出來。可是若是你把這道題拿給一個不懂編程的學生來作,就會發現大多數時候他們的作法會是下面的樣子:程序員

先對方程進行合併和簡化,最後再代入數值進行計算獲得結果就能夠了。有沒有發現事實上你本身在不寫代碼的時候也是這樣作的,由於你很清楚那些中間變量對於獲得正確的結果來講沒有什麼意義,而這樣解題效率更高,尤爲是當前面的環節和後面的環節能夠抵消掉某些互逆的運算時,這樣合併的好處可想而知github

而今天的主角【函數式編程】,能夠看作是這種思惟方式在程序設計中的應用,我並不建議非數學專業的做者從範疇論的角度去解釋函數式編程,由於術語運用的準確性會形成難以評估的影響,極可能達不到技術交流的目的,反而最終誤人子弟面試

三. 函數式編程

假如對某個需求的實現,須要傳入x,而後經歷3個步驟後獲得一個答案y,你會怎樣來實現呢?算法

3.1 傳統代碼的實現

這樣一個需求在傳統編程中最容易想到的就是鏈式調用:

function Task(value){
    this.value = value;
}

Task.prototype.step = function(fn){
    let _newValue = fn(this.value);
    return new Task(_newValue);
}
 
y = (new Task(x)).step(fn1).step(fn2).step(fn3);

你或許在jQuery中常常見到這樣的用法,或者你已經意識到上面的函數實際上就是Promise的簡化原型(關於Promise相關的知識能夠看《javascript基礎修煉(7)——Promise,異步,可靠性》這篇文章),只不過咱們把每一步驟包裹在了Task這個容器裏,每一個動做執行完之後返回一個新的Task容器,裏面裝着上一個步驟返回的結果。

3.2 函數式代碼推演

【函數式編程】,咱們再也不採用程序語言按照步驟來複現一個業務邏輯,而是換一個更爲抽象的角度,用數學的眼光看待所發生的事情。那麼上面的代碼實際上所作的事情就是:

經過一系列變換操做,講一個數據集x變成了數據集y

有沒有一點似曾相識的感受?沒錯,這就是咱們熟知的【方程】,或者【映射】:
$$
y=f(x)
$$
咱們將原來的代碼換個樣子,就更容易看出來了:

function prepare(){
    return function (x){
        return (new Task(x)).step(fn1).step(fn2).step(fn3);
    }    
}

let f = prepare();
let y = f(x);

上面的例子中,經過高階函數prepare( )將原來的函數改變爲一個延遲執行的,等待接收一個參數x並啓動一系列處理流程的新函數。再繼續進行代碼轉換,再來看一下f(x)執行到即將結束時的暫態情況:

//fn2Result是XX.step(fn2)執行完後返回的結果(值和方法都包含在Task容器中)
fn2Result.step(fn3);

上面的語句中,實際上變量只有fn2Resultstep()方法和fn10都是提早定義好的,那麼用函數化的思想來進行類比,這裏也是實現了一個數據集x1到數據集y1的映射,因此它也能夠被抽象爲y = f ( x )的模式:

//先生成一個用於生成新函數的高階函數,來實現局部調用
let goStep = function(fn){
    return function(params){
        let value = fn(params.value);
        return new Task(value);
    }
}
//fn2Result.step(fn3)這一句將被轉換爲以下形式
let requireFn2Result = goStep(fn3);

此處的requireFn2Result( )方法,只接受一個由前置步驟執行結束後獲得的暫態結果,而後將其關鍵屬性value傳入fn3進行運算並傳回一個支持繼續鏈式調用的容器。咱們來對代碼進行一下轉換:

function prepare(){
    return function (x){
        let fn2Result = (new Task(x)).step(fn1).step(fn2); 
        return requireFn2Result(fn2Result);
    }    
}

同理繼續來簡化前置步驟:

//暫時先忽略函數聲明的位置
let requireFn2Result = goStep(fn3);
let requireFn1Result = goStep(fn2);
let requireInitResult = goStep(fn1);

function prepare(){
    return function (x){
        let InitResult = new Task(x);
        return requireFn2Result(requireFn1Result(requireInitResult(InitResult)));
    }    
}

既然已經這樣了,索性再向前一步,把new Task(x)也函數化好了:

let createTask = function(x){
    return new Task(x);
};

3.3 函數化的代碼

或許你已經被上面的一系列轉化弄得暈頭轉向,咱們暫停一下,來看看函數化後的代碼變成了什麼樣子:

function prepare(){
    return function (x){
        return requireFn2Result(requireFn1Result(requireInitResult(createTask(x))));
    }    
}
let f = prepare();
let y = f(x);

這樣的編碼模式將核心業務邏輯在空間上放在一塊兒,而把具體的實現封裝起來,讓開發者更容易看到一個需求實現過程的全貌。

3.4 休息一下

不知道你是否有注意到,在中間環節的組裝過程當中,其實並無任何真實的數據出現,咱們只使用了暫態的抽象數據來幫助咱們寫出映射方法f的細節,而隨後暫態的數據又被新的函數取代,逐級迭代,直到暫態數據最終指向了最外層函數的形參,你能夠從新審視一下上面的推演過程來體會函數式編程帶來的變化,這個點是很是重要的。

3.5 進一步抽象

3.3節中函數化的代碼中,存在一個很長的嵌套調用,若是業務邏輯步驟過多,那麼這行代碼會變得很長,同時也很難閱讀,咱們須要經過一些手段將這些中間環節的函數展開爲一種扁平化的寫法。

/**
*定義一個工具函數compose,接受兩個函數做爲參數,返回一個新函數
*新函數接受一個x做爲入參,而後實現函數的迭代調用。
*/
var compose = function (f, g) {
    return function (x) {
        return f(g(x));
    }
};
/**
*升級版本的compose函數,接受一組函數,實現左側函數包裹右側函數的形態
*/
let composeEx = function (...args) {
    return (x)=>args.reduceRight((pre,cur)=>cur(pre),x);
}

看不懂的同窗須要補補基礎課了,須要注意的是工具函數返回的仍然是一個函數,咱們使用上面的工具函數來重寫一下3.3小節中的代碼:

let pipeline = composeEx(requireFn2Result,requireFn1Result,requireInitResult,createTask);
function prepare(){
    return function (x){
        return pipeline(x);
    }    
}
let f = prepare();
let y = f(x);

還要繼續?必須的,但願你尚未抓狂。代碼中咱們先執行prepare( )方法來獲得一個新函數ff執行時接收一個參數x,而後把x傳入pipeline方法,並返回pipeline(x)。咱們來進行一下對比:

//prepare執行後獲得的新函數
let f = x => pipeline(x);

或許你已經發現了問題所在,這裏的f函數至關於pipeline方法的代理,但這個代理什麼額外的動做都沒有作,至關於只是在函數調用棧中憑空增長了一層,可是執行了相同的動做。若是你可以理解這一點,就能夠得出下面的轉化結果:

let f = pipeline;

是否是很神奇?順便提一下,它的術語叫作point free,當你深刻學習【函數式編程】時就會接觸到。

3.6 完整的轉換代碼

咱們再進行一些簡易的抽象和整理,而後獲得完整的流程:

let composeEx = (...args) => (x) => args.reduceRight((pre,cur) =>cur(pre),x);
let getValue = (obj) => obj.value;
let createTask = (x) => new Task(x);
/*goStep執行後獲得的函數也知足前面提到的「let f=(x)=>g(x)」的形式,能夠將其pointfree化.
let goStep = (fn)=>(params)=>composeEx(createTask, fn, getValue)(params);
let requireFn2Result = goStep(fn3);
*/
let requireFn2Result = composeEx(createTask,fn3,getValue);
let requireFn1Result = composeEx(createTask,fn2,getValue);
let requireInitResult = composeEx(createTask,fn1,getValue);
let pipeline = composeEx(requireFn2Result,requireFn1Result,requireInitResult,createTask);
let f = pipeline;
let y = f(x);

能夠看到咱們定義完方法後,像搭積木同樣把它們組合在一塊兒,就獲得了一個能夠實現目標功能的函數。

3.7 爲何它看起來變得更復雜了

若是隻看上面的示例,的確是這樣的,上面的示例只是爲了展現函數式編程讓代碼向着怎樣一個方向去變化而已,而並無展現出函數式編程的優點,這種轉變和一個jQuery開發者剛開始使用諸如angular,vue,React框架時感覺到的強烈不適感是很類似的,畢竟思想的轉變是很是困難的。

面向對象編程寫出的代碼看起來就像是一個巨大的關係網和邏輯流程圖,好比連續讀其中10行代碼,你或許可以很清晰地看到某個步驟執行前和執行後程序的狀態,可是卻很難看清總體的業務邏輯流程;而函數式編程正好是相反的,你能夠在短短的10行代碼中看到整個業務流程,當你想去深究某個具體步驟時,再繼續展開,另外一方面,關注數據和函數組合能夠將你從複雜的this和對象的關係網中解放出來。

四. 兩個主角

數據函數【函數式編程】中的兩大核心概念,它爲咱們提供了用數學的眼光看世界的獨特視角,同時它也更程序員該有的思惟模式——設計程序,而不是僅僅是復現業務邏輯:

程序設計 = 數據結構 + 算法   Vs   函數式編程 = 數據 + 函數

但爲了更加安全有效地使用,它們和傳統編程中的同名概念相比多了一些限制。

函數Vs純函數

函數式編程中所傳遞和使用的函數,被要求爲【純函數】。純函數須要知足以下兩個條件:

  • 只依賴本身的參數
  • 執行過程沒有反作用

爲何純函數只能依賴本身的參數?由於只有這樣,咱們纔沒必要在對函數進行傳遞和組合的時候當心翼翼,生怕在某個環節弄丟了this的指向,若是this直接報錯還好,若是指向了錯誤的數據,程序自己在運行時也不會報錯,這種狀況的調試是很是使人頭疼的,除了逐行運行並檢查對應數據的狀態,幾乎沒什麼高效的方法。面向對象的編程中,咱們不得不使用不少bind函數來綁定一個函數的this指向,而純函數就不存在這樣的問題。來看這樣兩個函數:

var a = 1;
function inc(x){
    return a + x;
}
function pureInc(x){
    let a = 1;
    return x + a;
}

對於inc這個函數來講,改變外部條件a的值就會形成inc函數對於一樣的入參獲得不一樣的結果的狀況,換言之在入參肯定爲3的前提下,每次執行inc(3)獲得的結果是不肯定的,因此它是不純的。而pureInc函數就不依賴於外界條件的變化,pureInc(3)不管執行多少次,不管外界參數如何變化,其輸出結果都是肯定的。

在面向對象的編程中,咱們寫的函數一般都不是純函數,由於編程中或多或少都須要在不一樣的函數中共享一些標記狀態的變量,咱們更傾向與將其放在更高層的做用域裏,經過標識符的右查詢會沿做用域鏈尋找的機制來實現數據共享。

什麼是函數的反作用呢?一個函數執行過程對產生了外部可觀察的變化那麼就說這個函數是有反作用的。最多見的狀況就是函數接受一個對象做爲參數,可是在函數內部對其進行了修改,javascript中函數在傳遞對象參數時會將其地址傳入調用的函數,因此函數內部所作的修改也會同步反應到函數外部,這種反作用會在函數組合時形成最終數據的不可預測性,由於有關某個對象的函數都有可能獲得不肯定的輸出。

數據Vs不可變數據

javascript中的對象很強大也很靈活,可並非全部的場景中咱們都須要這種靈活性。來看這樣一個例子:

let a = {
    name:'tony'
}
let b = a;
modify(b);
console.log(a.name);

咱們沒法肯定上面的輸出結果,由於ab這兩個標識符指向了堆中的相同的地址,可外界沒法知道在modify函數中是否對b的屬性作出了修改。有些場景中爲了使得邏輯過程更加可靠,咱們不但願後續的操做和處理對最原始的數據形成影響,這個時候咱們很肯定須要拿到一個數據集的複製(好比拿到表格的總數據,在實現某些過濾功能的時候,一般須要留存一個表格數據的備份,以便取消過濾時能夠恢復原貌),這就引出了老生常談的深拷貝和淺拷貝的話題。

【深拷貝】是一種典型的防護性編程,由於在淺拷貝的機制下,修改對象屬性的時候會影響到全部指向它的標識符,從而形成不可預測的結果。

javascript中,常見的深拷貝都是經過遞歸來實現的,而後利用語言特性作出一些代碼層面的優化,例如各個第三方庫中的extend( )方法或者deepClone( )。但是當一個結構很深或者複雜度很高時,深拷貝的耗時就會大幅增長,有的時候咱們關注的可能只是數據結構中的一部分,也就是說新老對象中很大一部分數據是一致的,能夠共享的,但深拷貝過程當中忽視了這種狀況而簡單粗暴地對整個對象進行遞歸遍歷和克隆。

事實上【深拷貝】並非防護性編程的惟一方法,FacebookImmutable.js就用不可變數據的思路來解決這個問題,它將對象這種引用值變得更像原始值(javascript中的原始值建立後是不能修改的)。

//Immutable.js官網示例
 var map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
 var map2 = map1.set('b', 50);
 map1.get('b'); // 2
 map2.get('b'); // 50

你能夠查看【Immutable.js官方文檔】來了解如何使用它,一般它是結合React全家桶一塊兒使用的。若是你對其實現原理感興趣,能夠查看《深刻探究Immutable.js的實現機制》一文或者查看其餘資料,來了解一下Hash樹Trie樹是如何做爲Immutable的算法基礎而被應用的。

當標識符指向不變的數據,當函數沒有反作用,就能夠大膽普遍地使用函數式編程了

四. 前端的學習路線

  • javascript基礎

    若是你可以很清楚高階函數柯里化反柯里化這些關鍵詞的含義和通常用途,而且至少了解Arraymapreduce方法作了什麼事情,那麼就能夠進行下一步。不然就須要好好複習一下javascript的基礎知識。在javascript中進行函數式編程會反覆涉及到這些基本技術的運用。

  • 《javascript函數式編程指南》

    地址:https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/

    這是一原本自於gitbook的翻譯版的很是棒的開源電子書,這本書很棒,可是若是將函數式編程的相關知識分爲初中高級的話,這本書彷佛只涵蓋了初級和高級,而省略了中級的部分,當內容涉及到範疇論和代數結構的時候,理解難度會忽然一下變得很大。當你讀不懂的時候能夠先停下來,用下一個資料進行過渡,而後回過頭來再繼續閱讀後續的部分。

    同時提一句,翻譯者@鬍子大哈也是以前說起的那本著名的《React小書》的主要做者。

  • Ramda.js官網博文集

    地址:https://ramdajs.com/

    Ramda.jsjavascript提供了一系列函數式編程的工具函數,但官網的《Thinking In Ramda》系列教程,是很是好的中級教程,結合Ramda的API進行講解,讓開發者更容易理解函數式編程,它正好彌補了前一個資料中沒有中級教程的問題。

  • Ramda.js的API

    不得不說不少前端開發者都是從API開始學習函數式編程的,但很快就會發現學了和沒學差很少,由於沒有理論基礎,你很難知道該去使用它。就好像給了你最頂尖的工具,你也無法一次性就作出好吃的牛排,由於你不會作。

  • Rx.js和Immutable.js

    事實上筆者本身也尚未進行到這個階段的學習,Rx.js是隸屬於Angular全家桶的,Immutable.js是隸屬於React全家桶的,即便在本身目前的工做中沒有直接使用到,你也應該瞭解它們。

  • 代數結構的理論基礎

    地址:https://github.com/fantasyland/fantasy-land

    當你具有了基本的使用能力,想要更上一層樓的時候,就須要從新整合函數式編程的理論體系。這個項目用於解釋函數式編程的理論基礎中各種術語及相關用途。

五. 小結

【函數式編程】爲咱們展示了javascript語言的另外一種靈活性。

開發人員會發現本身能夠從更宏觀地角度來觀察整個業務流程,而不是往返於業務邏輯和實現細節之間。

測試人員會發現它很容易進行單元測試,不只由於它的純函數特性,也由於數據和動做被分離了。

遊戲玩家會發現它和本身在《個人世界》裏用方塊來搭建世界就是這樣子的。

工程師會發現它和對照零件圖紙編寫整個加工流水線的工藝流程時就是這樣作的。

數學家會說用數學的思惟是能夠描述世界的(若是你接觸過數學建模應該會更容易明白)。

【函數式編程】讓開發者理解程序設計這件事本質是是一種設計,是一種創造行爲,和其餘經過組合功能單元而獲得更強大的功能單元的行爲沒有本質區別。

相關文章
相關標籤/搜索