前端必學——函數語言程式設計(五)
前文梳理
第一篇
- 為什麼要進行函數語言程式設計?—— 一切只是為了程式碼更加可讀!!
- 開發人員喜歡【顯式】輸入輸出而不是【隱式】輸入輸出 ,要明白何為顯式,何為隱式!!
- 一個函式如果可以接受或返回一個甚至多個函式,它被叫做 高階函式 。閉包是最強大的高階函式!!
第二篇
講了重要的兩個概念: 偏函式 、 柯里化
- 函式組裝是函數語言程式設計最重要的實現方式!而熟練運用偏函式、柯里化,以及它們的變體,是函式組裝的基礎。
- 偏函式表現形式: 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
- Python 資料分析師的基本修養
- 設計模式之介面卡模式
- 如何做好企業數字化轉型?這10份靠譜案例收藏了(附下載)
- 效能提升400倍丨外匯掉期估值計算優化案例
- 如何面向物件程式設計?程式設計師:我也要先有“物件”啊
- 技術分享| 融合排程系統中的電子圍欄功能說明
- #yyds乾貨盤點# leetcode演算法題:環形連結串列 II
- 網站建設流程
- Java池化技術你瞭解多少?
- 如何實時計算日累計逐單資金流
- JAVA面試解析之Spring
- 一文參透分散式儲存系統Ceph的架構設計、叢集搭建(手把手)
- MQTT over QUIC:下一代物聯網標準協議為訊息傳輸場景注入新動力
- DataOps 不是工具,而是幫助企業實現資料價值的最佳實踐
- HTTP快取通天篇,可能有你想要的
- 使用 Tkinter 和 Python 製作文字編輯器
- logo去哪設計?lolgo設計方法分享!
- 效能提升30倍丨基於 DolphinDB 的 mytt 指標庫實現
- 資料庫擴容也可以如此絲滑,MySQL千億級資料生產環境擴容實戰
- 如何製作一個閃屏頁面