本篇是《JS如何函數式編程》系列第五篇!共七篇,彷佛已經能望見勝利的彼岸了!!!web
紀伯倫曾說過:咱們都已經走的過久了,以致於忘了爲何出發。編程
We already walked too far, down to we had forgotten why embarked.設計模式
因此,第五篇開始前,我們不如先來一個對前面每篇的梳理:數組
《XDM,JS如何函數式編程?看這就夠了!(一)》,做爲「綱要篇」,重點解釋了:性能優化
本系列是基於《medium 五萬贊好文-《我永遠不懂 JS 閉包》》和《「類」設計模式和「原型」設計模式——「複製」和「委託」的差別》兩篇的延伸探索,推薦閱讀。markdown
爲何要進行函數式編程?—— 一切只是爲了代碼更加可讀!!閉包
開發人員喜歡【顯式】輸入輸出而不是【隱式】輸入輸出,要明白何爲顯式,何爲隱式!!app
一個函數若是能夠接受或返回一個甚至多個函數,它被叫作高階函數。閉包是最強大的高階函數!!ide
《XDM,JS如何函數式編程?看這就夠了!(二)》,講了重要的兩個概念:偏函數、柯里化函數式編程
函數組裝是函數式編程最重要的實現方式!而熟練運用偏函數、柯里化,以及它們的變體,是函數組裝的基礎。
偏函數表現形式:partial(sum,1,2)(3)
柯里化表現形式:sum(1)(2)(3)
《XDM,JS如何函數式編程?看這就夠了!(三)》,來到了「函數組裝」這一重點:
再次重申,函數組裝是函數式編程最重要的實現方式!!
函數組裝符合 「聲明式編程風格」,即聲明的時候你就知道了它「是什麼」!而不用知道它具體「幹了什麼」(命令式函數風格)!
好比:當你看到組裝後的函數調用是這樣,compose( skipShortWords, unique, words )( text )
,就知道了它是先將 text 變成 words,而後 unique 去重,而後過濾較短長度的 words。很是清晰!
compose(..)
函數和 partial(..)
函數結合,能夠實現豐富多彩的組裝形式!
封裝抽象成函數是一門技術活!不能不夠,也不宜太過!
《XDM,JS如何函數式編程?看這就夠了!(四)》,咱們再細扣了下 「反作用」:
開發人員喜歡顯式輸入輸出而不是隱式輸入輸出,學函數式編程,這句話要深刻骨髓的記憶!
解決反作用的方法有:定義常量、明確 I/O、明確依賴、運用冪等,記得對冪等留個心眼!
咱們喜歡沒有反作用的函數,即純函數!!
假如一棵樹在森林裏倒下而沒有人在附近聽見,它有沒有發出聲音?——對於這個問題的理解就是:假如你封裝了一個高級函數,在內部即便有反作用的狀況下,外界會知道這個信息嗎,它還算是純函數嗎?
以上即是咱們的簡要回顧!
咱們可能還須要更多時間去實踐和體會:
partial(..)
和函數組裝 compose(..)
的變體及應用;OK!溫故知新,yyds!
第五篇,我們將基於實踐,分享最最多見的現象 —— 數組操做,看看它是如體現函數式編程精神!
這三劍客是:map(..)
、filter(..)
和 reduce(..)
。
咱們都會用 ES6 map(..)
, 它「是什麼」,咱們很是清楚!
輕鬆寫一個 map(..)
的使用:
[1,2,3].map(item => item + 1)
複製代碼
可是,map(..)
「幹了什麼」,即它的內部是怎樣的,你知道嗎?
咱們能夠用原生實現一個函數 map(..)
:
function map(mapperFn,arr) {
var newList = [];
for (let idx = 0; idx < arr.length; idx++) {
newList.push(
mapperFn( arr[idx], idx, arr )
);
}
return newList;
}
map(item=>item+1,[1,2,3])
複製代碼
咱們把一個 mapperFn(..)
封裝進模擬的 map(..)
函數內,其內部也是 for 循環遍歷。
咱們還能夠用 map(..)
作更多:
好比先將函數放在列表中,而後組合列表中的每個函數,最後執行它們,像這樣:
var increment = v => ++v;
var decrement = v => --v;
var square = v => v * v;
var double = v => v * 2;
[increment,decrement,square]
.map( fn => compose( fn, double ) )
.map( fn => fn( 3 ) );
// [7,5,36]
複製代碼
細細品一品~
若是說map(..)
的本質是映射值,filter(..)
的本質是過濾值。如圖示意:
[1,2,3].filter(item => item>2)
複製代碼
手寫一個 filter(..)
函數:
function filter(predicateFn,arr) {
var newList = [];
for (let idx = 0; idx < arr.length; idx++) {
if (predicateFn( arr[idx], idx, arr )) {
newList.push( arr[idx] );
}
}
return newList;
}
filter(item=>item>2,[1,2,3])
複製代碼
一樣也是將一個函數做爲入參,處理一樣傳入的 arr,遍歷過濾獲得目標數組;
map(..)
和 filter(..)
都會產生新的數組,而第三種操做(reduce(..))則是典型地將列表中的值合併(或減小)到單個值(非列表)。
[5,10,15].reduce( (product,v) => product * v, 3 );
複製代碼
過程:
手動實現 reduce 函數相較前兩個,要稍微複雜些:
function reduce(reducerFn,initialValue,arr) {
var acc, startIdx;
if (arguments.length == 3) {
acc = initialValue;
startIdx = 0;
}
else if (arr.length > 0) {
acc = arr[0];
startIdx = 1;
}
else {
throw new Error( "Must provide at least one value." );
}
for (let idx = startIdx; idx < arr.length; idx++) {
acc = reducerFn( acc, arr[idx], idx, arr );
}
return acc;
}
複製代碼
不像 map(..)
和 filter(..)
,對傳入數組的次序沒有要求。reduce(..)
明確要採用從左到右的處理方式。
基於 map(..)
、filter(..)
和 reduce(..)
,咱們再看些更復雜的操做;
實現:
var unique =
arr =>
arr.filter(
(v,idx) =>
arr.indexOf( v ) == idx
);
unique( [1,4,7,1,3,1,7,9,2,6,4,0,5,3] );
複製代碼
原理是,當從左往右篩選元素時,列表項的 idx 位置和 indexOf(..) 找到的位置相等時,代表該列表項第一次出現,在這種狀況下,將列表項加入到新數組中。
固然,去重方式有不少,可是,這種方式的優勢是,它們使用了內建的列表操做,它們能更方便的和其餘列表操做鏈式/組合調用。
這裏也寫一下reduce(..)
實現:
var unique =
arr =>
arr.reduce(
(list,v) =>
list.indexOf( v ) == -1 ?
( list.push( v ), list ) : list
, [] );
複製代碼
二位數組轉一維數組
[ [1, 2, 3], 4, 5, [6, [7, 8]] ] => [ 1, 2, 3, 4, 5, 6, 7, 8 ]
複製代碼
實現:
var flatten =
arr =>
arr.reduce(
(list,v) =>
list.concat( Array.isArray( v ) ? flatten( v ) : v )
, [] );
複製代碼
你還能夠加一個參數 depth 來指定降維的層數:
var flatten =
(arr,depth = Infinity) =>
arr.reduce(
(list,v) =>
list.concat(
depth > 0 ?
(depth > 1 && Array.isArray( v ) ?
flatten( v, depth - 1 ) :
v
) :
[v]
)
, [] );
flatten( [[0,1],2,3,[4,[5,6,7],[8,[9,[10,[11,12],13]]]]], 2 );
// [0,1,2,3,4,5,6,7,8,[9,[10,[11,12],13]]]
複製代碼
看到這裏,若是以爲複雜,你能夠只把它做爲一個庫來調用便可。實際上,咱們後續還會專門來介紹各種函數式編程函數庫!
仔細體會下,如下給出的三段代碼,哪段你以爲你更容易看懂?哪一段更符合函數式編程?
// 實現 1
[1,2,3,4,5]
.filter( isOdd )
.map( double )
.reduce( sum, 0 ); // 18
// 實現 2
reduce(
map(
filter( [1,2,3,4,5], isOdd ),
double
),
sum,
0
); // 18
// 實現 3
compose(
partialRight( reduce, sum, 0 ),
partialRight( map, double ),
partialRight( filter, isOdd )
)
( [1,2,3,4,5] ); // 18
複製代碼
在片斷 1 和 片斷 3 中沒法抉擇?
再看一例:
var removeInvalidChars = str => str.replace( /[^\w]*/g, "" );
var upper = str => str.toUpperCase();
var elide = str =>
str.length > 10 ?
str.substr( 0, 7 ) + "..." :
str;
var words = "Mr. Jones isn't responsible for this disaster!"
.split( /\s/ );
words;
// ["Mr.","Jones","isn't","responsible","for","this","disaster!"]
// 片斷 1
words
.map( removeInvalidChars )
.map( upper )
.map( elide );
// ["MR","JONES","ISNT","RESPONS...","FOR","THIS","DISASTER"]
// 片斷 3
words
.map(
compose( elide, upper, removeInvalidChars )
);
// ["MR","JONES","ISNT","RESPONS...","FOR","THIS","DISASTER"]
複製代碼
重點就是:
咱們能夠將那三個獨立的相鄰的 map(..) 調用步驟當作一個轉換組合。由於它們都是一元函數,而且每個返回值都是下一個點輸入值。咱們能夠採用 compose(..) 執行映射功能,並將這個組合函數傳入到單個 map(..) 中調用:
因此:片斷 3 這種融合的技術,是常見的性能優化方式。
以上,咱們看到了:
三個強大通用的列表操做:
這是咱們日常用的最多的數組遍歷方式,但此次咱們藉助函數式編程思想把它們升級了!
這些高級操做:unique(..)、flatten(..)、map 融合的思想等(其實還有不少其它高級操做),值得咱們去研究、感覺體會,最後運用到實踐中去!!
OK~ 本次就到這裏,期待下次再會 ~
我是掘金安東尼,公衆號【掘金安東尼】,輸入暴露輸入,技術洞見生活!