1. 程式人生 > >JavaScript 函數語言程式設計到底是個啥

JavaScript 函數語言程式設計到底是個啥

隨著大前端時代的到來,在產品開發過程中,前端所佔業務比重越來越大、互動越來越重。傳統的老夫拿起JQuery就是一把梭應付當下重互動頁面已經十分乏力。於是乎有了Angular,React,Vue這些現代框架。

但隨之而來的還有大量的新知識新名詞,如MVC,MVVM,Flux這些設計模式就弄得很多同學傻傻分不清。這時候又見到別人討論什麼函數語言程式設計,更是一臉懵逼了。

我們大多聽過面向物件程式設計,面向過程程式設計,那啥又是函數語言程式設計呢?在我們前端開發中又有哪些應用場景?我抱著這個疑惑,初步的學習了下。 (此文僅是學習,無甚乾貨)。

函數語言程式設計

定義

函數語言程式設計(Functional Programming,後面簡稱FP),維基百科的定義是:

是一種程式設計範型,它將電腦運算視為數學上的函式計算,並且避免使用程式狀態以及易變物件。函式程式語言最重要的基礎是λ演算(lambda calculus)。而且λ演算的函式可以接受函式當作輸入(引數)和輸出(傳出值)。比起指令式程式設計,函數語言程式設計更加強調程式執行的結果而非執行的過程,倡導利用若干簡單的執行單元讓計算結果不斷漸進,逐層推導複雜的運算,而不是設計一個複雜的執行過程。

我來嘗試理解下這個定義,好像就是說,在敲程式碼的時候,我要把過程邏輯寫成函式,定義好輸入引數,只關心它的輸出結果。而且可以把函式作為輸入輸出。感覺好像平常寫js時,就是這樣的嘛!

特性

網上FP的定義與特性琳琅滿目。各種百科、部落格、一些老師的網站上都有大同小異的介紹。為了方便閱讀,我列下幾個好像比較重要的特性,並附上我的第一眼理解。

  1. 函式是一等公民。就是說函式可以跟其他變數一樣,可以作為其他函式的輸入輸出。喔,回撥函式就是典型應用。

  2. 不可變數。就是說,不能用var跟let咯。按這要求,我似乎有點難寫程式碼。

  3. 純函式。就是沒有副作用的函式。這個好理解,就是不修改函式外部的變數。

  4. 引用透明。這個也好理解,就是說同樣的輸入,必定是同樣的輸出。函式內部不依賴外部狀態,如一些全域性變數。

  5. 惰性計算。大意就是:一個表示式繫結的變數,不是宣告的時候就計算出來,而是真正用到它的時候才去計算。

還有一些衍生的特性,如柯里化與組合,三言兩語說不清,就不闡述了,有興趣的同學可以自己再瞭解瞭解。

FP在JavaScript中的應用

React就是典型的FP。它不同於Vue這樣的MVVM框架,它僅僅是個View層。

ReactView = render(data) 它只關心你的輸入,最終給你返回相應檢視。所以你休想在react元件中去修改父元件的狀態,更沒有與dom的雙向繫結。

這個是框架上的應用,那麼在我們平常書寫JavaScript時有哪些應用呢?換句話說,平常書寫js時候,遇到什麼情況,我們採用FP會更好。

從最常見的入手吧,如典型的運算元組:

// 從users中篩選出年齡大於15歲的人的名字
const users = [
  {
    age: 10,
    name: '張三',
  }, {
    age: 20,
    name: '李四'
  }, {
    age: 30,
    name: '王五'
  }
];

// 過程式
const names = [];
for (let i = 0; i < users.length; i++)    {
  if (users[i].age > 15) {
    names.push(users[i].name);
  }
}
// 函式式
const names = users.filter(u => u.age > 15).map(u => u.name);

嗯,程式碼精簡了很多,但是貌似帶來了更大的開銷。如果是非常大的資料,非常多的篩選工作,那就會迴圈多次。

這裡得想到剛剛的惰性計算。按照惰性求值的要求,應該是要最後返回結果時,才真正去篩選年紀並得到姓名陣列。

然而JavaScript的陣列並不支援惰性求值。這時候我們得上一些工具庫,如 Lodash 。可以看下它文件中的例子: _.chain 。

好像也沒好到哪裡去啊,不就是把多行程式碼變一行嘛?說的那麼玄乎,還多了效能開銷,然後又跟我說得上個工具庫。。。

說的好像很有道理,但是for迴圈是有個弊端的,它產生了變數i,而這個變數又是不可控的,如果業務邏輯一複雜,誰知道它迴圈到什麼時候i有沒有發生變化,然後導致迴圈出問題呢?

我們再看一個與DOM互動的場景:

假如頁面有一個按鈕 button ,我們需要求出使用者點選了幾次,但是一秒鐘內重複點選的不算。傳統方法會這麼寫。

var count = 0;
var rate = 1000;
var lastClick = Date.now() - rate;
var button = document.querySelector('button');
button.addEventListener('click', () => {
  if (Date.now() - lastClick >= rate) {
    console.log(`Clicked ${++count} times`);
    lastClick = Date.now();
  }
});

妥,完全沒問題。但是發現多了很多狀態,count,rate,lastClick,還得對比來對比去。那如果用FP會是怎麼樣的呢?

抱歉。。。沒法寫。。。除非很強大的程式設計能力,自己封裝好方法去處理。所以在這裡,我們可以上個工具--- Rx.js ,上述的例子就是rxjs中引用的,我們看它是如何優雅地處理的。

var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
  .throttleTime(1000) // 每隔1000毫秒才能觸發事件
  .scan(count => count + 1, 0) // 求值,預設值是0
  .subscribe(count => console.log(`Clicked ${count} times`)); // 訂閱結果、輸出值

巧奪天工!再也不用去管理狀態了,不需要宣告一堆變數,修改來修改去,判斷來判斷去,簡直完美。

平常我們有很多需要更新dom的非同步操作,如搜尋行為:使用者連續輸入查詢值,如果停頓半秒就執行搜尋,如果搜尋了多次,發起了多次請求,那隻返回最終輸入的那次搜尋結果。

閉上眼想想,你之前是怎麼實現的。反正我都是設定開始時間,結束時間,上次時間,等等變數。繁瑣,而且不可控。

當我們以FP的思想去實現時,就會想方設法的減少變數,來優雅程式。最常見的方法就是用下別人的工具庫來實現它。當然有些簡單的場景也可以自己實現,最主要的還是要有這個意識。

其實我們平常已經寫了一些FP了,只是我們沒意識到,或者沒怎麼寫好。就好比閉包,很多人都不瞭解閉包的概念,但實際上已經寫了很多閉包程式碼。其實閉包本身也是函數語言程式設計的一個應用。

鑑於我自己理解也不深,沒法多闡述FP的應用,大家如果有興趣,可以多瞭解瞭解。

FP在JavaScript中的優劣勢

總結一下FP的優劣,以便於我們在實際開發中,能更好的抉擇是否採用FP。

優勢

  1. 更好的管理狀態。因為它的宗旨是無狀態,或者說更少的狀態。而平常DOM的開發中,因為DOM的視覺呈現依託於狀態變化,所以不可避免的產生了非常多的狀態,而且不同元件可能還相互依賴。以FP來程式設計,能最大化的減少這些未知、優化程式碼、減少出錯情況。

  2. 更簡單的複用。極端的FP程式碼應該是每一行程式碼都是一個函式,當然我們不需要這麼極端。我們儘量的把過程邏輯以更純的函式來實現,固定輸入->固定輸出,沒有其他外部變數影響,並且無副作用。這樣程式碼複用時,完全不需要考慮它的內部實現和外部影響。

  3. 更優雅的組合。往大的說,網頁是由各個元件組成的。往小的說,一個函式也可能是由多個小函式組成的。參考上面第二點,更強的複用性,帶來更強大的組合性。

  4. 隱性好處。減少程式碼量,提高維護性。

劣勢

  1. JavaScript不能算是嚴格意義上的函式式語言,很多函數語言程式設計的特性並沒有。比如上文說的陣列的惰性鏈求值。為了實現它就得上工具庫,或者自己封裝實現,提高了程式碼編寫成本。

  2. 跟過程式相比,它並沒有提高效能。有些地方,如果強制用FP去寫,由於沒有中間變數,還可能會降低效能。

  3. 程式碼不易讀。這個因人而異,因碼而已。特別熟悉FP的人可能會覺得這段程式碼一目瞭然。而不熟悉的人,遇到寫的晦澀的程式碼,看著一堆堆lambda演算跟匿名函式 () => () => () 瞬間就懵逼了。看懂程式碼,得腦子裡先演算半小時。

  4. 學習成本高。一方面繼承於上一點。另一方面,很多前端coder,就是因為相對不喜歡一些底層的抽象的程式語言,才來踏入前端坑,你現在又讓他們一頭扎入FP,顯得手足無措。

總結

個人覺得,FP還是好的。對於開發而言,確確實實能優化我們的程式碼,熟悉之後,也能提高程式設計效率。對於程式設計本身而言,也能拓展我們的思維,不侷限在過程式的程式設計程式碼。