譯者按: 有時候一個算法的直觀、簡潔、高效是須要做出取捨的。javascript
本文采用意譯,版權歸原做者全部java
函數式編程中用於操做數組的方法就像「毒品」同樣,它讓不少人愛上函數式編程。由於它們真的十分經常使用並且又超級簡單。 .map()
和 .filter()
都僅需一個參數,該參數定義操做數組每個元素的函數便可。reduce()
會複雜一些,我以前寫過一篇文章介紹爲何人們難以掌握reduce()
方法,其中一個緣由在於不少入門資料都僅僅用算術做爲例子。我寫了不少用reduce()
來作算術之外的例子。算法
用reduce()
來計算數組的平均值是一個經常使用的模式。代碼看起來很是簡單,不過在計算最終結果以前你須要作兩個準備工做:編程
這兩個事情看起來都很簡單,那麼計算數組的平均值並非很難了吧。解法以下:小程序
function average(nums) { return nums.reduce((a, b) => a + b) / nums.length; }
確實不是很難,是吧?可是若是數據結構變得複雜了,就沒那麼簡單了。好比,數組裏面的元素是對象,你須要先過濾掉某些對象,而後從對象中取出數字。這樣的場景讓計算平均值變得複雜了一點。微信小程序
接下來咱們處理一個相似的問題(從this Free Code Camp challenge得到靈感),咱們會提供 5 種不一樣的解法,每一種方法有各自的優勢和缺點。這 5 種方法也展現了 JavaScript 的靈活。我但願能夠給你在使用reduce
的實戰中一些靈感。數組
假設咱們有一個數組,記錄了維多利亞時代經常使用的口語。接下來咱們要找出那些依然現存於 Google Books 中的詞彙,並計算他們的平均流行度。數據的格式是這樣的:微信
const victorianSlang = [ { term: "doing the bear", found: true, popularity: 108 }, { term: "katterzem", found: false, popularity: null }, { term: "bone shaker", found: true, popularity: 609 }, { term: "smothering a parrot", found: false, popularity: null }, { term: "damfino", found: true, popularity: 232 }, { term: "rain napper", found: false, popularity: null }, { term: "donkey’s breakfast", found: true, popularity: 787 }, { term: "rational costume", found: true, popularity: 513 }, { term: "mind the grease", found: true, popularity: 154 } ];
接下來咱們用 5 中不一樣的方法計算平均流行度值。數據結構
初次嘗試,咱們不使用reduce()
。若是你對數組的經常使用函數不熟悉,用 for 循環可讓你更好地理解咱們要作什麼。app
let popularitySum = 0; let itemsFound = 0; const len = victorianSlang.length; let item = null; for (let i = 0; i < len; i++) { item = victorianSlang[i]; if (item.found) { popularitySum = item.popularity + popularitySum; itemsFound = itemsFound + 1; } } const averagePopularity = popularitySum / itemsFound; console.log("Average popularity:", averagePopularity);
若是你熟悉 JavaScript,上面的代碼理解起來應該很容易:
polularitySum
和itemsFound
變量。popularitySum
記錄總的流行度值,itemsFound
記錄咱們已經找到的全部的條目;len
和item
來幫助咱們遍歷數組;i
的值,直到循環n
次;vitorianSlang[i]
;popularity
並累加到popularitySum
;itemsFound
;popularitySum
除以itemsFound
來計算平均值。代碼雖然不是那麼簡潔,可是順利完成了任務。使用數組迭代方法能夠更加簡潔,接下來開始吧…..
咱們首先將這個問題拆分紅幾個子問題:
fitler()
找到那些在 Google Books 中的條目;map()
獲取流行度;reuduce()
來計算總的流行度;下面是實現代碼:
// 輔助函數 // ---------------------------------------------------------------------------- function isFound(item) { return item.found; } function getPopularity(item) { return item.popularity; } function addScores(runningTotal, popularity) { return runningTotal + popularity; } // 計算 // ---------------------------------------------------------------------------- // 找出全部isFound爲true的條目 const foundSlangTerms = victorianSlang.filter(isFound); // 從條目中獲取流行度值,返回爲數組 const popularityScores = foundSlangTerms.map(getPopularity); // 求和 const scoresTotal = popularityScores.reduce(addScores, 0); // 計算平均值 const averagePopularity = scoresTotal / popularityScores.length; console.log("Average popularity:", averagePopularity);
注意看addScores
函數以及調用reduce()
函數的那一行。addScores()
接收兩個參數,第一個runningTotal
,咱們把它叫作累加數,它一直記錄着累加的總數。每訪問數組中的一個條目,咱們都會用addScores
函數來更新它的值。第二個參數popularity
是當前某個元素的值。注意,第一次調用的時候,咱們尚未runningTotal
的值,因此在調用reduce()
的時候,咱們給runningTotal
初始化。也就是reduce()
的第二個參數。
這個版本的代碼簡潔不少了,也更加的直觀。咱們再也不告訴 JavaScript 引擎如何循環,如何對當前索引的值作操做。咱們定義了不少小的輔助函數,而且把它們組合起來完成任務。filter()
,map()
和reduce()
幫咱們作了不少工做。上面的實現更加直觀地告訴咱們這段代碼要作什麼,而不是底層如何去實現。
在以前的版本中,咱們建立了不少中間變量:foundSlangTerms
,popularityScores
。接下來,咱們給本身設一個挑戰,使用鏈式操做,將全部的函數調用組合起來,再也不使用中間變量。注意:popularityScores.length
變量須要用其它的方式來獲取。咱們能夠在addScores
的累加參數中記錄它。
// 輔助函數 // --------------------------------------------------------------------------------- function isFound(item) { return item.found; } function getPopularity(item) { return item.popularity; } // 咱們使用一個對象來記錄總的流行度和條目的總數 function addScores({ totalPopularity, itemCount }, popularity) { return { totalPopularity: totalPopularity + popularity, itemCount: itemCount + 1 }; } // 計算 // --------------------------------------------------------------------------------- const initialInfo = { totalPopularity: 0, itemCount: 0 }; const popularityInfo = victorianSlang .filter(isFound) .map(getPopularity) .reduce(addScores, initialInfo); const { totalPopularity, itemCount } = popularityInfo; const averagePopularity = totalPopularity / itemCount; console.log("Average popularity:", averagePopularity);
咱們在reduce
函數中使用對象來記錄了totalPopularity
和itemCount
。在addScores
中,每次都更新itemCount
的計數。
經過filter
,map
和reduce
計算的最終的結果存儲在popularityInfo
中。你甚至能夠繼續簡化上述代碼,移除沒必要要的中間變量,讓最終的計算代碼只有一行。
注意: 若是你不熟悉函數式語言或則以爲難以理解,請跳過這部分!
若是你熟悉curry()
和compose()
,接下來的內容就不難理解。若是你想知道更多,能夠看看這篇文章: ‘A Gentle Introduction to Functional JavaScript’. 特別是第三部分 。
咱們可使用compose
函數來構建一個徹底不帶任何變量的代碼,這就叫作point-free
的方式。不過,咱們須要一些幫助函數。
// 輔助函數 // ---------------------------------------------------------------------------- const filter = p => a => a.filter(p); const map = f => a => a.map(f); const prop = k => x => x[k]; const reduce = r => i => a => a.reduce(r, i); const compose = (...fns) => arg => fns.reduceRight((arg, fn) => fn(arg), arg); // The blackbird combinator. // See: https://jrsinclair.com/articles/2019/compose-js-functions-multiple-parameters/ const B1 = f => g => h => x => f(g(x))(h(x)); // 計算 // ---------------------------------------------------------------------------- // 求和函數 const sum = reduce((a, i) => a + i)(0); // 計算數組長度的函數 const length = a => a.length; // 除法函數 const div = a => b => a / b; // 咱們使用compose()來將函數組合起來 // compose()的參數你能夠倒着讀,來理解程序的含義 const calcPopularity = compose( B1(div)(sum)(length), map(prop("popularity")), filter(prop("found")) ); const averagePopularity = calcPopularity(victorianSlang); console.log("Average popularity:", averagePopularity);
咱們在compose
中作了全部的計算。從後往前看,首先filter(prop('found'))
篩選出全部在 Google Books 中的條目,而後經過map(prop('popularity'))
獲取全部的流行度數值,最後使用 magical blackbird (B1
) combinator 來對同一個輸入進行sum
和length
的計算,並求得平均值。
// All the lines below are equivalent: const avg1 = B1(div)(sum)(length); const avg2 = arr => div(sum(arr))(length(arr)); const avg3 = arr => sum(arr) / length(arr); const avg4 = arr => arr.reduce((a, x) => a + x, 0) / arr.length;
不要擔憂看不明白,上面主要是爲你們演示有 4 種方式來實現average
功能。這就是 JavaScript 的優美之處。
相對來講,本文的內容是有點極客的。雖然筆者以前深度使用函數式語言 Haskell 作過很多研究項目,對函數式很有理解,可是 point-free 風格的代碼,咱們是不建議在實際工程中使用的,維護成本會很高。咱們Fundebug全部的代碼都要求直觀易懂,不推崇用一些奇淫技巧來實現。除非某些萬不得已的地方,可是必定要把註釋寫得很是清楚,來下降後期的維護成本。
以前全部的解法均可以很好地工做。那些使用reduce()
的解法都有一個共同點,它們將大的問題拆解問小的子問題,而後經過不一樣的方式將它們組合起來。可是也要注意它們對數組遍歷了三次,感受很沒有效率。若是一次就能夠計算出來,纔是最佳的方案。確實能夠,不過須要一點數學運算。
爲了計算 n 個元素的平均值,咱們使用下面的公式:
那麼,計算 n+1 個元素的平均值,使用一樣的公式(惟一不一樣的是 n 變成 n+1):
它等同於:
一樣等同於:
作點變換:
結論是,咱們能夠一直記錄當前狀態下的全部知足條件的元素的平均值。只要咱們知道以前全部元素的平均值和元素的個數。
// 求平均值 function averageScores({ avg, n }, slangTermInfo) { if (!slangTermInfo.found) { return { avg, n }; } return { avg: (slangTermInfo.popularity + n * avg) / (n + 1), n: n + 1 }; } const initialVals = { avg: 0, n: 0 }; const averagePopularity = victorianSlang.reduce(averageScores, initialVals).avg; console.log("Average popularity:", averagePopularity);
這個方法只須要遍歷一次就計算出平均值,缺點是咱們作了更多的計算。每一次當元素知足條件,都要作乘法和除法,而不是最後才作一次除法。不過,它使用了更少的內存,由於沒有中間的數組變量,咱們只是記錄了一個僅僅有兩個元素的對象。
這樣寫還有一個缺點,代碼一點都不直觀,後續維護麻煩。至少一眼看過去不能理解它是作什麼的。
因此,到底哪種方案纔是最好的呢?視情形而定。也許你有一個很大的數組要處理,也許你的代碼須要在內存很小的硬件上跑。在這些場景下,使用第 5 個方案最佳。若是性能不是問題,那麼就算使用最低效的方法也沒問題。你須要選擇最適合的。
還有一些聰明的朋友會思考:是否能夠將問題拆解爲子問題,仍然只遍歷一次呢?是的,確實有。須要使用 transducer。
Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟件、百姓網等衆多品牌企業。歡迎你們免費試用!
轉載時請註明做者Fundebug以及本文地址:
https://blog.fundebug.com/2019/06/05/5-ways-calculate-an-average-with-reduce/