相關係列: 從零開始的前端築基之旅(面試必備,持續更新~)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
複製代碼
再也不指示計算機如何工做,而是指出咱們明確但願獲得的結果。與命令式不一樣,聲明式意味着咱們要寫表達式,而不是一步一步的指示。github
以 SQL 爲例,它就沒有「先作這個,再作那個」的命令,有的只是一個指明咱們想要從數據庫取什麼數據的表達式。至於如何取數據則是由它本身決定的。之後數據庫升級也好,SQL 引擎優化也好,根本不須要更改查詢語句。web
這是函數式編程的核心概念:面試
// 比較 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
可緩存性。
純函數可以根據輸入來作緩存。
可移植性/自文檔化。
****
可測試性(Testable)
純函數讓測試更加容易。咱們不須要僞造一個「真實的」支付網關,或者每一次測試以前都要配置、以後都要斷言狀態(assert the state)。只需簡單地給函數一個輸入,而後斷言輸出就行了。
合理性(Reasonable)
不少人相信使用純函數最大的好處是_引用透明性_(referential transparency)。若是一段代碼能夠替換成它執行所得的結果,並且是在不改變整個程序行爲的前提下替換的,那麼咱們就說這段代碼是引用透明的。
因爲純函數老是可以根據相同的輸入返回相同的輸出,因此它們就可以保證老是返回同一個結果,這也就保證了引用透明性。
並行代碼
咱們能夠並行運行任意純函數。由於純函數根本不須要訪問共享的內存,並且根據其定義,純函數也不會因反作用而進入競爭態(race condition)。
面嚮對象語言的問題是,它們永遠都要隨身攜帶那些隱式的環境。你只須要一個香蕉,但卻獲得一個拿着香蕉的大猩猩...以及整個叢林
函數只在須要的時候執行,不產生無心義的中間變量。從頭至尾都在寫函數,只有在最後的時候才經過調用 產生實際的結果。
函數式編程中有兩種操做是必不可少的:柯里化(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 的用處很是普遍,就像在 hasSpaces
、findSpaces
和 censored
看到的那樣,只需傳給函數一些參數,就能獲得一個新函數。
用 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
複製代碼
因此,若是咱們想把字符串變爲大寫(假設head
,reverse
,toUpperCase
函數存在),能夠這麼寫:
compose(toUpperCase, compose(head, reverse));
// 或者
compose(compose(toUpperCase, head), reverse);
複製代碼
結合律的一大好處是任何一個函數分組均可以被拆開來,而後再以它們本身的組合方式打包在一塊兒。關於如何組合,並無標準的答案——咱們只是以本身喜歡的方式搭樂高積木罷了。
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 組合的時候遇到了困難,那麼可使用下面這個實用的,可是不純的 trace
函數來追蹤代碼的執行狀況。
var trace = curry(function(tag, x){
console.log(tag, x);
return x;
});
複製代碼
前端領域,咱們能看到不少函數式編程的影子:ES6 中加入了箭頭函數,Redux 引入 Elm 思路下降 Flux 的複雜性,React16.6 開始推出 React.memo(),使得 pure functional components 成爲可能,16.8 開始主推 Hook,建議使用 pure function 進行組件編寫……
若是你收穫了新知識,請在作側邊欄第一個按鈕用力點一下~
參考文檔: