這篇文章不會大講什麼是函數式編程,爲何要使用函數式編程,而是開門見山,介紹函數式編程的最佳實踐,但願你們讀完了這篇文章,會理解函數式編程的美妙而且愛上它。javascript
純函數是這樣一種函數:對於一樣的輸入,老是會產生一樣的輸出,沒有反作用, 儘可能在你的代碼中使用純函數,這會使你的代碼更加健壯,測試更加容易。java
接下來來一塊兒看看各類反作用node
看下面這段代碼ajax
function foo(x) {
y = x * 2;
}
var y;
foo( 3 );
複製代碼
分析上面的代碼,調用函數改變了函數外的變量y, 產生了反作用,所以不是純函數編程
當一個函數引用了函數外的變量,也就是自由變量,並非全部的自由變量引用都是糟糕的,可是咱們處理的時候必須很是當心api
咱們能夠很是容易地將他變成純函數數組
function foo(x) {
return x*2;
}
foo( 3 );
複製代碼
隨機數也會產生反作用,對於一樣的輸入,結果是沒法預測的bash
最多見的反作用是輸入輸出,一個程序沒有IO是徹底沒有意義的,由於它的工做不能用任何方式觀測到,舉個最多見的例子app
var users = {};
function fetchUserData(userId) {
ajax( `http://some.api/user/${userId}`, function onUserData(user){
users[userId] = user;
} );
}
複製代碼
fetchUserData改變了users, 想變得更純一點,咱們能夠建立一個包裹函數safer_fetchUserData
,將外部變量和不純的函數都包裹起來函數式編程
function safer_fetchUserData(userId,users) {
// 拷貝一份外部變量
users = Object.assign( {}, users );
fetchUserData( userId );
return users;
// ***********************
// 原始的非純函數:
function fetchUserData(userId) {
ajax(
`http://some.api/user/${userId}`,
function onUserData(user){
users[userId] = user;
}
);
}
}
複製代碼
safer_fetchUserData
更純一點,可是依然不是純函數,由於依賴ajax調用的結果,ajax的反作用是沒法消除的 從上面也能夠看出,反作用沒法徹底消除,咱們只能儘量地寫純函數,將不純的部分都收集在一塊兒
考慮下面這段代碼
["1","2","3"].map( parseInt );
複製代碼
相信不少人都會不假思索的回答[1,2,3], 可是真實的結果是[1, NaN, NaN], 認真思考一下array.map(fn)這個高階函數的執行過程,在每一輪的迭代中,fn函數都會執行,執行的時候會傳入三個參數, 分別是數組的這一輪的元素,索引,和數組自己,因此真實的執行狀況是
parseInt("1", 0, ["1","2","3"])
parseInt("2", 1, ["1","2","3"])
parseInt("3", 2, ["1","2","3"])
複製代碼
parseInt(x,radix)能夠接受兩個參數,由於第三個參數忽略,第一個參數x表明要轉換的值, 第二個參數radix是轉換進制,當參數 radix 的值爲 0,或沒有設置該參數時,parseInt() 會根據 string 來判斷數字的基數。
當忽略參數 radix , JavaScript 默認數字的基數以下:
若是 string 以 "0x" 開頭,parseInt() 會把 string 的其他部分解析爲十六進制的整數。
若是 string 以 0 開頭,那麼 ECMAScript v3 容許 parseInt() 的一個實現把其後的字符解析爲八進制或十六進制的數字。
若是 string 以 1 ~ 9 的數字開頭,parseInt() 將把它解析爲十進制的整數。 因此最後結果是[1, NaN, NaN]也就不足爲奇了
那麼如何解決這個問題,返回預期的結果, 估計不少人不假思索都會想出這種方式
["1","2","3"].map(v => parseInt(v));
複製代碼
沒錯,的確能夠獲得正確的結果,簡單粗暴,直接明瞭,可是這種方式是否是能夠稍微封裝一下,具備擴展性 在函數式編程中,能夠用這個一元轉換函數包裹目標函數,確保目標函數只會接受一個參數
function unary(fn) {
return function onlyOneArg(arg){
return fn(arg);
};
}
["1","2","3"].map( unary(parseInt) ); // [1,2,3]
複製代碼
固定一個函數的一個或者多個參數,返回一個新的函數,這個函數用於接受剩餘的參數, 和map結合使用比較多
function partial(fn, ...presetArgs) {
return function partiallyApplied(...laterArgs ) {
return fn(...presetArgs, ...laterArgs )
}
}
// 應用1
var getPerson = partial( ajax, "http://some.api/person" )
var getOrder = partial( ajax, "http://some.api/order" )
// version1
var getCurrentUser = partial(ajax, "http://some.api/person", {user: "hello world"})
// version 2
var getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } );
// 應用2
function add(x, y){
return x + y
}
[1,2,3,4,5].map( partial( add, 3 ) )
複製代碼
function partialRight(fn, ...presetArgs) {
return function partiallyApplied(...laterArgs) {
return fn(...laterArgs, ...presetArgs)
}
}
複製代碼
柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。
function add(a, b) {
return a + b;
}
// 執行 add 函數,一次傳入兩個參數便可
add(1, 2) // 3
// 假設有一個 curry 函數能夠作到柯里化
var addCurry = curry(add);
addCurry(1)(2) // 3
複製代碼
下面是一個簡單的實現
function sub_curry(fn) {
var args = [].slice.call(arguments, 1);
return function() {
return fn.apply(this, args.concat([].slice.call(arguments)));
};
}
function curry(fn, length) {
length = length || fn.length;
var slice = Array.prototype.slice;
return function() {
if (arguments.length < length) {
var combined = [fn].concat(slice.call(arguments));
return curry(sub_curry.apply(this, combined), length - arguments.length);
} else {
return fn.apply(this, arguments);
}
};
}
複製代碼
函數組合,將一個函數的輸出當成另一個函數的輸入,讓數據流能夠像水在水管中流動同樣,爲了組合,必須保證組合的函數參數只能有一個,並且必須有返回值
複製代碼
// 執行順序從右向左
function compose(...fn) {
return function composed(result){
var list = [...fn]
while(list.length > 0) {
result = list.pop()(result)
}
return result
}
}
// 管道函數, 從左向右移動
function pipe(...fn) {
return function piped(result) {
var list = [...fn]
while(list.length > 0) {
result = list.shift()(result)
}
return result
}
}
複製代碼
// 判斷一個數是否是素數
function isPrime(num,divisor = 2){
if (num < 2 || (num > 2 && num % divisor == 0)) {
return false;
}
if (divisor <= Math.sqrt( num )) {
return isPrime( num, divisor + 1 );
}
return true;
}
// 計算二叉樹的深度
function depth(node) {
if(node) {
let depthLeft = depth(node.left)
let depthRight = depth(node.right)
return 1 + Math.max(depthLeft, depthLeft)
}
return 0
}
複製代碼
// 解決棧溢出的問題,尾調用優化
// 尾調用的概念很是簡單,就是指某個函數的最後一步是調用另外一個函數。
// 下面都不是
// 狀況一
function f(x){
let y = g(x);
return y;
}
// 狀況二
function f(x){
return g(x) + 1;
}
// 階乘函數
function factorial(n) {
if( n === 1) {
return 1
}
return n*factorial(n-1)
}
複製代碼
將階乘函數改爲尾調用, 確保最後一步只調用自身, 就是把全部用到的內部變量改寫成函數的參數
function factorial(n, total) {
if (n===1) {
return total
}
return factorial(n - 1, n*total)
}
// 可是這樣會傳兩個參數,用兩個函數改寫一下
function factorial(n) {
return tailFactorial(n ,1)
}
function tailFactorial(n, total) {
if (n===1) {
return total
}
return tailFactorial(n - 1, n*total)
}
// 繼續改寫, tailFactorial放在factorial內部
function factorial(n) {
function tailFactorial(n, total) {
if (n===1) {
return total
}
return tailFactorial(n - 1, n*total)
}
return tailFactorial(n ,1)
}
// 也可使用curry函數,將多參數的函數轉換爲單參數的形式
function currying(fn, n) {
return function (m) {
return fn(m, n);
};
}
function tailFactorial(n, total) {
if (n===1) {
return total
}
return tailFactorial(n - 1, n*total)
}
var factorial = currying(tailFactorial, 1)
factorial(5)
複製代碼