1. 程式人生 > >js中的函數編程

js中的函數編程

棧溢出 如果 turn urn 編譯 結合 以及 好的 cti

之前在網上看到了一篇教你如何用js寫出裝逼的代碼。

經過學些以及擴展很有收獲在這裏記錄一下。

原文章找不到了。所以就不在這附上鏈接了。

大家看下下面兩段js代碼。

技術分享圖片技術分享圖片

上面兩端代碼效果是一模一樣的,都是在一個指定的數組中,找到指定的數字所在的下標。第一個大多數人都看得懂。第二個就不一定了。

這裏就帶大家從一步步最初的版本演化到最終的函數式的版本。

希望大家以後再遇到如此難以閱讀的代碼,知道怎麽去理解。

為了從上面上面變到下面,我們需要了解以下知識。

箭頭函數

三元運算符(不講)

尾遞歸優化

匿名函數(不講)

柯裏化

高階函數

其中,尾遞歸優化,和柯裏化,高階函數,都是函數式編程裏面的東西。所以有必要先簡單介紹一下什麽是函數式編程。

這裏先引用下知乎的圖。

技術分享圖片

上面的圖並不是說,最下面的最高級,而是越靠下面的越費腦子。

什麽是函數式編程?

事實上函數式編程是從範疇論(category theory)發展過來的。而範疇論其實和微積分,數理邏輯,等等都是一種數學理論。而函數式編程只是參考這種思想發展過來的,就像設計模式最初是來源於建築學一樣。

為了更好的理解函數式編程,這裏也再簡單的介紹下範疇論。高深的我也不懂。。。

在維基百科裏面是這麽定義的

技術分享圖片

也就是說只要是存在某種關系,可以從一個對象轉化為另外一個對象,那麽對象和它們之間的關系就構成一個範疇。

下面的圖是一個示意圖。

技術分享圖片

紅色的點和黃色的箭頭在一起就構成一個範疇。

箭頭arrow,還有一個正式的名字叫做態射(morphism)。範疇論認為,同一個範疇的所有成員,就是不同狀態的"變形"(transformation)。通過"態射",一個成員可以變形成另一個成員。

上面的很抽象,我們舉例子說明下。

大家都對面向對象的思想比較了解,在面向對象的思想裏,萬事萬物都是對象,而對象又可以抽象成為類。比如,黃種人,白種人,黑人。都是對象,可以抽象為人這個類。這是他們有共性。但是他們之間並不存在轉化關系,黑人不可能轉化為白種人,就算他整容整的很白,概念上他還是個黑人。

而數字1,2,3,4,5。。。他們之間存在一定的關系,1+1可以變成2,2+1可變成3,2+2又可以變成4。我們可以認為,數字和它們之間的態射就是一個範疇。每一個數字,通過一個態勢可以變為另外一個數字。

所以範疇包含兩部分

  1. 成員
  2. 關系

而過度到程序裏面就是

  1. 函數

簡單的來說就是,一個值可以通過一個函數變為另外一個值。所以函數式編程要求每一個函數必須是幹凈的,進入一個值,出去另外一個值。不會操作任何方法外的數據。

函數式編程有兩個最基本的運算。

  1. 合成

技術分享圖片

合成的概念就是如果一個值需要經過多個函數才能變成另外一個值,就可以把兩個函數合成一個函數。比如1,需要經過add1和add2才能變成4,那麽就可以合成一個add3出來。

假設add1=f,add2=g

技術分享圖片

上面看起來函數有點多,那我們把add1和add2都給匿名了,看起來會好一點

技術分享圖片

函數的合成還要滿足結合律

技術分享圖片

假設f,g,h分別是add1,add2,add3.

那麽(h·g).f 就是 add3(add1(add2)) 而h·(g·f) 就是add1(add2(add3))

他們兩者應該是相等的。

2.柯裏化

f(x)和g(x)合成為f(g(x)),有一個隱藏的前提,就是f和g都只能接受一個參數。如果可以接受多個參數,比如f(x, y)和g(a, b, c),函數合成就非常麻煩。

這時就需要函數柯裏化了。所謂"柯裏化",就是把一個多參數的函數,轉化為單參數函數。

技術分享圖片

這裏解釋一下,柯裏化之後是采用的js裏面的鏈式調用。AddX(2)其實返回的是function(x)

{return x+2;} 這時候再跟一個(1),就是把1傳入裏面執行了。

當然函數式編程還有很多別的東西,這裏就不一一介紹了,有興趣的的可以自己查下。

下面說下, 尾遞歸優化

我們知道遞歸的害處,那就是如果遞歸很深的話,stack受不了,並會導致性能大幅度下降。所以,我們使用尾遞歸優化技術——每次遞歸時都會重用stack,這樣一來能夠提升性能,當然,這需要語言或編譯器的支持。Java 就不支持,但是javascript支持。而所謂的支持,就是說編譯器會自動優化,對於尾遞歸的代碼會自動優化成。

普通遞歸。

技術分享圖片

下面是入棧和出棧的過程。會保存上一步的計算狀態,太深的話就會棧溢出。

fac(5)

(5*fac(4))

(5*(4*fac(3)))

(5*(4*(3*fac(2))))

(5*(4*(3*(2*fac(1)))))

(5*(4*(3*2)))

(5*(4*(6)))

(5*24)

120

尾遞歸

當遞歸調用是整個函數體中最後執行的語句且它的返回值不屬於表達式的一部分時,這個遞歸調用就是尾遞歸

技術分享圖片

每次都是執行一個獨立的函數,和之前的函數並沒有關聯。

fac(5,1)

fac(4,5)

fac(3,20)

fac(2,60)

fac(1,120)

120

普通遞歸創建stack累積而後計算收縮,尾遞歸只會占用恒量的內存。只需要保存每次計算出來的值,然後傳入同一個函數就好。

高階函數。

高階函數就是函數當參數,把傳入的函數做一個封裝,然後返回這個封裝函數。上面一直都在用到。

箭頭函數

ECMAScript2015 引入的箭頭表達式。箭頭函數其實都是匿名函數。簡單的理解就是通過箭頭創建函數。

一個參數時候可以省略小括號,方法體只有一句的時候可以省略大括號

Function (x){return x+1}; 等價於(x) =>{return x+1}或者x=>return x+1;

無參數必須有括號。

Function (){return 1+1} 等價於 ()=>{return 1+1}

如果想加名字的話。

Var add = ()=>{return 1+1}; 調用 add() 就會返回2

該介紹的都介紹了,下面就一步步改造。

原始版本

技術分享圖片

尾遞歸優化之後

技術分享圖片

替換三元運算符

技術分享圖片

函數體內的函數參數化

技術分享圖片

轉化為箭頭函數

技術分享圖片

匿名

技術分享圖片

引入高階函數,並柯裏化。

技術分享圖片

為了方便調用再加上名字

技術分享圖片

下面斷點圖幫助理解。

技術分享圖片

js中的函數編程