懶人整理的js函數式編程講解

相關係列: 從零開始的前端築基之旅(面試必備,持續更新~)javascript

什麼是函數式編程

  • 是一種編程範型,它將電腦運算視爲數學上的函數計算,而且避免使用程序狀態以及易變對象。
  • 函數式編程更增強調程序執行的結果而非執行的過程,倡導利用若干簡單的執行單元讓計算結果不斷漸進,逐層推導複雜的運算,而不是設計一個複雜的執行過程。
  • 函數式編程的思惟過程是徹底不一樣的,它的着眼點是函數,而不是過程,它強調的是如何經過函數的組合變換去解決問題,而不是我經過寫什麼樣的語句去解決問題

爲何叫函數式編程

根據學術上函數的定義,函數便是一種描述集合和集合之間的轉換關係,輸入經過函數都會返回有且只有一個輸出值。函數其實是一個關係,或者說是一種映射,而這種映射關係是能夠組合的。前端

在咱們的編程世界中,咱們須要處理的其實也只有「數據」和「關係」,而關係就是函數。咱們所謂的編程工做也不過就是在找一種映射關係,一旦關係找到了,問題就解決了,剩下的事情,就是讓數據流過這種關係,而後轉換成另外一個數據。java

函數式編程的特色

函數是一等公民

你能夠像對待任何其餘數據類型同樣對待它們——把它們存在數組裏,看成參數傳遞,賦值給變量...等等。使用總有返回值的表達式而不是語句git

// 函數式編程-函數做爲返回參數
const add = (x) => {
  return plus = (y) => {
    return x + y;
  }
};
let plus1 = add(1);
let plus2 = add(2);

console.log(plus1(1)); // 2
console.log(plus2(1)); // 3
複製代碼

聲明式編程 (Declarative Programming)

再也不指示計算機如何工做,而是指出咱們明確但願獲得的結果。與命令式不一樣,聲明式意味着咱們要寫表達式,而不是一步一步的指示。github

以 SQL 爲例,它就沒有「先作這個,再作那個」的命令,有的只是一個指明咱們想要從數據庫取什麼數據的表達式。至於如何取數據則是由它本身決定的。之後數據庫升級也好,SQL 引擎優化也好,根本不須要更改查詢語句。web

無狀態和數據不可變 (Statelessness and Immutable data)

這是函數式編程的核心概念:面試

  • 數據不可變: 它要求你全部的數據都是不可變的,這意味着若是你想修改一個對象,那你應該建立一個新的對象用來修改,而不是修改已有的對象。
  • 無狀態: 主要是強調對於一個函數,無論你什麼時候運行,它都應該像第一次運行同樣,給定相同的輸入,給出相同的輸出,徹底不依賴外部狀態的變化。
// 比較 Array 中的 slice 和 splice
let test = [1, 2, 3, 4, 5];

// slice 爲純函數,返回一個新的數組
console.log(test.slice(0, 3)); // [1, 2, 3]
console.log(test); // [1, 2, 3, 4, 5]

// splice則會修改參數數組
console.log(test.splice(0, 3)); // [1, 2, 3]
console.log(test); // [4, 5]
複製代碼

函數應該純自然,無反作用

純函數是這樣一種函數,即相同的輸入,永遠會獲得相同的輸出,並且沒有任何可觀察的反作用。數據庫

反作用是指,函數內部與外部互動,產生運算之外的其餘結果。 例如在函數調用的過程當中,利用並修改到了外部的變量,那麼就是一個有反作用的函數。編程

反作用可能包含,但不限於:segmentfault

  • 更改文件系統
  • 往數據庫插入記錄
  • 發送一個 http 請求
  • 可變數據
  • 打印/log
  • 獲取用戶輸入
  • DOM 查詢
  • 訪問系統狀態

純函數的優勢:

  1. 可緩存性。

    純函數可以根據輸入來作緩存。

  2. 可移植性/自文檔化。

    • 可移植性能夠意味着把函數序列化(serializing)並經過 socket 發送。也能夠意味着代碼可以在 web workers 中運行。
    • 純函數是徹底自給自足的,它須要的全部東西都能輕易得到。純函數的依賴很明確,所以更易於觀察和理解

    ****

  3. 可測試性(Testable)

    純函數讓測試更加容易。咱們不須要僞造一個「真實的」支付網關,或者每一次測試以前都要配置、以後都要斷言狀態(assert the state)。只需簡單地給函數一個輸入,而後斷言輸出就行了。

  4. 合理性(Reasonable)

    不少人相信使用純函數最大的好處是_引用透明性_(referential transparency)。若是一段代碼能夠替換成它執行所得的結果,並且是在不改變整個程序行爲的前提下替換的,那麼咱們就說這段代碼是引用透明的。

    因爲純函數老是可以根據相同的輸入返回相同的輸出,因此它們就可以保證老是返回同一個結果,這也就保證了引用透明性。

  5. 並行代碼

    咱們能夠並行運行任意純函數。由於純函數根本不須要訪問共享的內存,並且根據其定義,純函數也不會因反作用而進入競爭態(race condition)。

面嚮對象語言的問題是,它們永遠都要隨身攜帶那些隱式的環境。你只須要一個香蕉,但卻獲得一個拿着香蕉的大猩猩...以及整個叢林

惰性執行(Lazy Evaluation)

函數只在須要的時候執行,不產生無心義的中間變量。從頭至尾都在寫函數,只有在最後的時候才經過調用 產生實際的結果。

函數式編程中有兩種操做是必不可少的:柯里化(Currying)函數組合(Compose)

柯里化

把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。

函數式編程 + 柯里化,將提取成柯里化的函數部分配置好以後,可做爲參數傳入,簡化操做流程。

// 給 list 中每一個元素先加 1,再加 5,再減 1
let list = [1, 2, 3, 4, 5];

//正常作法
let list1 = list.map((value) => {
  return value + 1;
});
let list2 = list1.map((value) => {
  return value + 5;
});
let list3 = list2.map((value) => {
  return value - 1;
});
console.log(list3); // [6, 7, 8, 9, 10]

// 柯里化
const changeList = (num) => {
  return (data) => {
    return data + num
  }
};
let list1 = list.map(changeList(1)).map(changeList(5)).map(changeList(-1));
console.log(list1); // [6, 7, 8, 9, 10]
複製代碼

返回的函數就經過閉包的方式記住了傳入的第一個參數

一次次地調用它實在是有點繁瑣,咱們可使用一個特殊的 curry 幫助函數(helper function)使這類函數的定義和調用更加容易。

var curry = require('lodash').curry;

var match = curry(function(what, str) {
  return str.match(what);
});

var replace = curry(function(what, replacement, str) {
  return str.replace(what, replacement);
});

var filter = curry(function(f, ary) {
  return ary.filter(f);
});

var map = curry(function(f, ary) {
  return ary.map(f);
});
複製代碼

上面的代碼中遵循的是一種簡單,同時也很是重要的模式。即策略性地把要操做的數據(String, Array)放到最後一個參數裏。

你能夠一次性地調用 curry 函數,也能夠每次只傳一個參數分屢次調用。

match(/\s+/g, "hello world");
// [ ' ' ]

match(/\s+/g)("hello world");
// [ ' ' ]

var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }

hasSpaces("hello world");
// [ ' ' ]

hasSpaces("spaceless");
// null
複製代碼

這裏代表的是一種「預加載」函數的能力,經過傳遞一到兩個參數調用函數,就能獲得一個記住了這些參數的新函數。

curry 的用處很是普遍,就像在 hasSpacesfindSpacescensored 看到的那樣,只需傳給函數一些參數,就能獲得一個新函數。

map 簡單地把參數是單個元素的函數包裹一下,就能把它轉換成參數爲數組的函數。

var getChildren = function(x) {
  return x.childNodes;
};

var allTheChildren = map(getChildren);
複製代碼

只傳給函數一部分參數一般也叫作_局部調用_(partial application),可以大量減小樣板文件代碼(boilerplate code)。

當咱們談論_純函數_的時候,咱們說它們接受一個輸入返回一個輸出。curry 函數所作的正是這樣:每傳遞一個參數調用函數,就返回一個新函數處理剩餘的參數。這就是一個輸入對應一個輸出啊。哪怕輸出是另外一個函數,它也是純函數。

函數組合

函數組合的目的是將多個函數組合成一個函數。

const compose = (f, g) => {
  return (x) => {
    return f(g(x));
  };
};
複製代碼

compose 的定義中,g 將先於 f 執行,所以就建立了一個從右到左的數據流。組合的概念直接來自於數學課本,從右向左執行更加可以反映數學上的含義。

全部的組合都有一個特性

// 結合律(associativity)
var associative = compose(f, compose(g, h)) == compose(compose(f, g), h);
// true
複製代碼

因此,若是咱們想把字符串變爲大寫(假設headreversetoUpperCase 函數存在),能夠這麼寫:

compose(toUpperCase, compose(head, reverse));

// 或者
compose(compose(toUpperCase, head), reverse);
複製代碼

結合律的一大好處是任何一個函數分組均可以被拆開來,而後再以它們本身的組合方式打包在一塊兒。關於如何組合,並無標準的答案——咱們只是以本身喜歡的方式搭樂高積木罷了。

pointfree

pointfree 模式指的是,函數無須說起將要操做的數據是什麼樣的。一等公民的函數、柯里化(curry)以及組合協做起來很是有助於實現這種模式。

// 非 pointfree,由於提到了數據:word
var snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_');
};

// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
複製代碼

利用 curry,咱們可以作到讓每一個函數都先接收數據,而後操做數據,最後再把數據傳遞到下一個函數那裏去。另外注意在 pointfree 版本中,不須要 word 參數就能構造函數;而在非 pointfree 的版本中,必需要有 word 才能進行一切操做。pointfree 模式可以幫助咱們減小沒必要要的命名,讓代碼保持簡潔和通用。

debug

若是在 debug 組合的時候遇到了困難,那麼可使用下面這個實用的,可是不純的 trace 函數來追蹤代碼的執行狀況。

var trace = curry(function(tag, x){
  console.log(tag, x);
  return x;
});
複製代碼

優點

  1. 更好的管理狀態。由於它的宗旨是無狀態,或者說更少的狀態。而日常DOM的開發中,由於DOM的視覺呈現依託於狀態變化,因此不可避免的產生了很是多的狀態,並且不一樣組件可能還相互依賴。以FP來編程,能最大化的減小這些未知、優化代碼、減小出錯狀況。
  2. 更簡單的複用。極端的FP代碼應該是每一行代碼都是一個函數,固然咱們不須要這麼極端。咱們儘可能的把過程邏輯以更純的函數來實現,固定輸入->固定輸出,沒有其餘外部變量影響,而且無反作用。這樣代碼複用時,徹底不須要考慮它的內部實現和外部影響。
  3. 更優雅的組合。往大的說,網頁是由各個組件組成的。往小的說,一個函數也多是由多個小函數組成的。參考上面第二點,更強的複用性,帶來更強大的組合性。
  4. 隱性好處。減小代碼量,提升維護性。

缺點

  1. 性能:函數式編程相每每會對一個方法進行過分包裝,從而產生上下文切換的性能開銷。同時,在 JS 這種非函數式語言中,函數式的方式必然會比直接寫語句指令慢(引擎會針對不少指令作特別優化)。
  2. 資源佔用:在 JS 中爲了實現對象狀態的不可變,每每會建立新的對象,所以,它對垃圾回收(Garbage Collection)所產生的壓力遠遠超過其餘編程方式。這在某些場合會產生十分嚴重的問題。
  3. 遞歸陷阱:在函數式編程中,爲了實現迭代,一般會採用遞歸操做,爲了減小遞歸的性能開銷,咱們每每會把遞歸寫成尾遞歸形式,以便讓解析器進行優化。可是衆所周知,JS 是不支持尾遞歸優化的.
  4. 代碼不易讀。特別熟悉FP的人可能會以爲這段代碼一目瞭然。而不熟悉的人,遇到寫的晦澀的代碼,看懂代碼,得腦子裏先演算半小時。

前端領域,咱們能看到不少函數式編程的影子:ES6 中加入了箭頭函數,Redux 引入 Elm 思路下降 Flux 的複雜性,React16.6 開始推出 React.memo(),使得 pure functional components 成爲可能,16.8 開始主推 Hook,建議使用 pure function 進行組件編寫……

若是你收穫了新知識,請在作側邊欄第一個按鈕用力點一下~

參考文檔:

  1. JavaScript函數式編程
  2. JavaScript 函數式編程究竟是個啥
  3. 函數式編程指北
相關文章
相關標籤/搜索