译者按: 有时候一个算法的直观、简洁、高效是须要做出取舍的。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/