前端必學——函數式編程(五)
前文梳理
第一篇
- 為什麼要進行函數式編程?—— 一切只是為了代碼更加可讀!!
- 開發人員喜歡【顯式】輸入輸出而不是【隱式】輸入輸出 ,要明白何為顯式,何為隱式!!
- 一個函數如果可以接受或返回一個甚至多個函數,它被叫做 高階函數 。閉包是最強大的高階函數!!
第二篇
講了重要的兩個概念: 偏函數 、 柯里化
- 函數組裝是函數式編程最重要的實現方式!而熟練運用偏函數、柯里化,以及它們的變體,是函數組裝的基礎。
- 偏函數表現形式: partial(sum,1,2)(3)
- 柯里化表現形式: sum(1)(2)(3)
第三篇
“函數組裝”這一重點:
- 再次重申, 函數組裝是函數式編程最重要的實現方式!!
- 函數組裝符合 “聲明式編程風格” ,即聲明的時候你就知道了它“是什麼”!而不用知道它具體“幹了什麼”(命令式函數風格)!
- 比如:當你看到組裝後的函數調用是這樣, compose( skipShortWords, unique, words )( text ) ,就知道了它是先將 text 變成 words,然後 unique 去重,然後過濾較短長度的 words。非常清晰!
- compose(..) 函數和 partial(..) 函數結合,可以實現豐富多彩的組裝形式!
- 封裝抽象成函數是一門技術活!不能不夠,也不宜太過!
第四篇
再細扣了下 “副作用” :
- 開發人員喜歡顯式輸入輸出而不是隱式輸入輸出,學函數式編程,這句話要深入骨髓的記憶!
- 解決副作用的方法有:定義常量、明確 I/O、明確依賴、運用冪等,記得對冪等留個心眼!
- 我們喜歡沒有副作用的函數,即純函數!!
- 假如一棵樹在森林裏倒下而沒有人在附近聽見,它有沒有發出聲音? ——對於這個問題的理解就是:假如你封裝了一個高級函數,在內部即使有副作用的情況下,外界會知道這個信息嗎,它還算是純函數嗎?
以上便是我們的簡要回顧!
我們可能還需要更多時間去實踐和體會:
- 偏函數 partial(..) 和函數組裝 compose(..) 的變體及應用;
- 抽象的能力;
- 封裝高級的純函數;
第五篇,咱們將基於實踐,分享最最常見的現象 —— 數組操作 ,看看它是如體現函數式編程精神!
數組三劍客
這三劍客是: map(..) 、 filter(..) 和 reduce(..) 。
map
我們都會用 ES6 map(..) , 它“是什麼”,我們非常清楚!
輕鬆寫一個 map(..) 的使用:
[1,2,3].map(item => item + 1)
但是, map(..) “幹了什麼”,即它的內部是怎樣的,你知道嗎?
我們可以用原生實現一個函數 map(..) :
function map(mapperFn,arr) { var newList = []; for (let id = 0; id < arr.length; id++) { newList.push( mapperFn( arr[id], id, 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]
細細品一品~
filter
如果説 map(..) 的本質是映射值, filter(..) 的本質是過濾值。如圖示意:
[1,2,3].filter(item => item>2)
手寫一個 filter(..) 函數:
function filter(predicateFn,arr) { var newList = []; for (let id = 0; id < arr.length; id++) { if (predicateFn( arr[id], id, arr )) { newList.push( arr[id] ); } } return newList; } filter(item=>item>2,[1,2,3])
同樣也是將一個函數作為入參,處理同樣傳入的 arr,遍歷過濾得到目標數組;
reduce
map(..) 和 filter(..) 都會產生新的數組,而第三種操作(reduce(..))則是典型地將列表中的值合併(或減少)到單個值(非列表)。
[5,10,15].reduce( (product,v) => product * v, 3 );
過程:
- 3 * 5 = 15
- 15 * 10 = 150
- 150 * 15 = 2250
手動實現 reduce 函數相較前兩個,要稍微複雜些:
function reduce(reducerFn,initialValue,arr) { var acc, startId; if (arguments.length == 3) { acc = initialValue; startId = 0; } else if (arr.length > 0) { acc = arr[0]; startId = 1; } else { throw new Error( "Must provide at least one value." ); } for (let id = startId; id < arr.length; id++) { acc = reducerFn( acc, arr[id], id, arr ); } return acc; }
不像 map(..) 和 filter(..) ,對傳入數組的次序沒有要求。 reduce(..) 明確要採用從左到右的處理方式。
高級操作
基於 map(..) 、 filter(..) 和 reduce(..) ,我們再看些更復雜的操作;
去重
實現:
var unique = arr => arr.filter( (v,id) => arr.indexOf( v ) == id ); unique( [1,4,7,1,3,1,7,9,2,6,4,0,5,3] );
原理是,當從左往右篩選元素時,列表項的 id 位置和 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 這種 融合 的技術,是常見的性能優化方式。
階段小結
以上,我們看到了:
三個強大通用的列表操作:
- map(..): 轉換列表項的值到新列表;
- filter(..): 選擇或過濾掉列表項的值到新數組;
- reduce(..): 合併列表中的值,並且產生一個其他的值(也可能是非列表的值);
這是我們平常用的最多的數組遍歷方式,但這次我們藉助函數式編程思想把它們升級了!
這些高級操作:unique(..)、flatten(..)、map 融合的思想等(其實還有很多其它高級操作),值得我們去研究、感受體會,最後運用到實踐中去!!
我是掘金安東尼: 一名人氣前端技術博主(文章 100w+ 閲讀量)
終身寫作者(INFP 寫作人格)
堅持與熱愛(簡書打卡 1000 日)
我能陪你一起度過漫長技術歲月嗎(以夢為馬)
覺得不錯,給個點贊和關注吧(這是我最大的動力 )b( ̄▽ ̄)d
- 開源雲真機平台-Sonic應用實踐
- Android 自定義View - 柱狀波形圖 wave view
- ArcGIS製圖技巧:製圖入門與點、線、面狀符號製作
- Android技術分享|【Android踩坑】懷疑人生,主線程修改UI也會崩潰?
- 數據分表Mybatis Plus動態表名最優方案的探索
- 個人開源項目如何上傳maven中央倉庫
- 遊戲創作者能夠為玩家創造出快樂的體驗,這或許就是做遊戲的魅力吧!
- 用Python自動生成 圖文並茂的數據分析 報吿
- 軟件測試之測試代表用户
- 分佈式前修課:Zookeeper鎖實現方式
- Python 數據分析師的基本修養
- 設計模式之適配器模式
- 如何做好企業數字化轉型?這10份靠譜案例收藏了(附下載)
- 性能提升400倍丨外匯掉期估值計算優化案例
- 如何面向對象編程?程序員:我也要先有“對象”啊
- 技術分享| 融合調度系統中的電子圍欄功能説明
- #yyds乾貨盤點# leetcode算法題:環形鏈表 II
- 網站建設流程
- Java池化技術你瞭解多少?
- 如何實時計算日累計逐單資金流