功能性JavaScript:查找数组元素和.reduce()方法的算术平均值的五种方法

遍历数组的方法类似于“启动药物”(当然,它们不是药物;我并不是说药物是好药;它们只是一种比喻)。 因此,许多人“坐下来”进行函数式编程。 问题是它们非常方便。 此外,这些方法大多数都很容易理解。 .map().filter()仅接受一个回调参数,并允许您解决简单的问题。 但是,感觉到.reduce()方法给许多人带来了一些困难。 要了解它会有点困难。



我已经写了关于为什么我认为.reduce()会带来很多问题的文章。 部分原因在于许多手册仅在处理数字时演示了.reduce()的使用。 因此,我写了关于使用.reduce()可以解决多少个不暗示算术运算的任务。 但是,如果您绝对需要使用数字怎么办?

.reduce()典型用法看起来像是计算数组元素的算术平均值。 乍一看,此任务似乎没有什么特别之处。 但是她不是那么简单。 事实是,在计算平均值之前,您需要找到以下指标:

  1. 数组元素值的总数。
  2. 数组的长度。

找出所有这些非常简单。 而且,计算数字数组的平均值也不是一件容易的事。 这是一个基本示例:

 function average(nums) {    return nums.reduce((a, b) => (a + b)) / nums.length; } 

如您所见,这里没有特殊的理解。 但是,如果您必须使用更复杂的数据结构,则任务将变得更加困难。 如果我们有一组对象怎么办? 如果该数组中的某些对象需要过滤怎么办? 如果需要从对象中提取某些数值该怎么办? 在这种情况下,计算数组元素的平均值已经有点复杂了。

为了解决这个问题,我们将解决培训问题(它是基于FreeCodeCamp的任务)。 我们将以五种不同的方式解决它。 它们每个都有自己的优点和缺点。 对这5种解决此问题的方法的分析将显示JavaScript的灵活性。 我希望对解决方案的分析能为您在实际项目中如何使用.reduce()提供参考。

任务概述


假设我们有一个描述维多利亚时代的lang语表达式的对象数组。 您需要过滤掉在Google图书中找不到的那些表达式(相应对象的found属性为false ),并为这些表达式的受欢迎程度找到一个平均评分。 此类数据如下所示( 从此处获取 ):

 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种方法来找到评估此数组中表达式流行度的平均值。

1.不使用.reduce()解决问题(命令循环)


在我们解决问题的第一种方法中,将不使用.reduce()方法。 如果您以前没有遇到过迭代数组的方法,那么我希望通过解析此示例可以为您澄清一下情况。

 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,那么您将轻松理解此示例。 实际上,这里发生以下情况:

  1. 我们初始化变量itemsFounditemsFound 。 第一个变量popularitySum ,存储表达式受欢迎程度的总体评分。 第二个变量itemsFound (很惊讶)存储找到的表达式数量。
  2. 然后,我们初始化常量len和变量item ,这对遍历数组很有用。
  3. for循环中,计数器i递增,直到其值达到数组最后一个元素的索引值为止。
  4. 在循环内部,我们获取要探索的数组元素。 我们使用victorianSlang[i]构造访问元素。
  5. 然后,我们确定是否在藏书中找到了该表达方式。
  6. 如果在书本中出现表达式,我们将采用其受欢迎程度等级的值,并将其添加到变量popularitySum的值中。
  7. 同时,我们还增加了找到的表达式itemsFound的计数器。
  8. 最后,我们通过将itemsFound除以itemsFound找到平均值。

因此,我们应付了任务。 也许我们的决定不是特别漂亮,但确实可以做到。 使用方法遍历数组将使其更加整洁。 让我们看一下我们是否成功,事实是,要“清理”这一决定。

2.简单的解决方案1:.filter()、. map()并使用.reduce()查找数量


让我们在第一次尝试使用数组的方法来解决问题之前,将其分成小部分。 即,这是我们需要做的:

  1. 选择表示Google图书集中的表达式的对象。 在这里,您可以使用.filter()方法。
  2. 从对象中提取表达受欢迎程度的评估。 要解决此子任务, .map()方法是合适的。
  3. 计算等级的总和。 在这里,我们可以求助于我们的老朋友.reduce()的帮助。
  4. 最后,找到估计的平均值。

这是代码中的样子:

 //   // ---------------------------------------------------------------------------- function isFound(item) {    return item.found; }; function getPopularity(item) {    return item.popularity; } function addScores(runningTotal, popularity) {    return runningTotal + popularity; } //  // ---------------------------------------------------------------------------- //  ,      . const foundSlangTerms = victorianSlang.filter(isFound); //   ,   . const popularityScores = foundSlangTerms.map(getPopularity); //     .    ,    //   ,  reduce     ,  0. const scoresTotal = popularityScores.reduce(addScores, 0); //       . const averagePopularity = scoresTotal / popularityScores.length; console.log("Average popularity:", averagePopularity); 

看一下addScore函数,以及addScore .reduce()的行。 请注意, addScore接受两个参数。 第一个, runningTotal ,称为电池。 它存储值的总和。 每当我们遍历数组并执行return时,它的值都会更改。 第二个参数popularity是我们正在处理的数组的单独元素。 在addScore数组的最开始, addScoreaddScore return从未执行过。 这意味着runningTotal没有自动设置runningTotal 。 因此,通过调用.reduce() ,我们将一开始需要传递给runningTotal的值传递给该方法。 这是传递给.reduce()的第二个参数。

因此,我们应用了迭代数组的方法来解决该问题。 该解决方案的新版本比以前的解决方案干净得多。 换句话说,该决定更具声明性。 我们不会向JavaScript确切说明如何执行循环;我们不会遵循数组元素的索引。 相反,我们声明了小尺寸的简单辅助函数并将其组合在一起。 数组方法.filter() .map().filter()为我们完成了所有艰苦的工作。 解决此类问题的这种方法更具表现力。 这些数组方法比循环所能完成的更加完整,它们告诉我们代码中规定的意图。

3.简易解决方案2:使用多个电池


在该解决方案的先前版本中,我们创建了一堆中间变量。 例如, foundSlangTermsfoundSlangTerms 。 在我们的情况下,这样的解决方案是完全可以接受的。 但是,如果我们为代码设计设定一个更复杂的目标怎么办? 如果我们可以在程序中使用流利的界面设计模式,那就太好了。 使用这种方法,我们将能够链接所有函数的调用,并且无需中间变量就可以执行此操作。 但是,这里有一个问题在等待着我们。 请注意,我们需要获取PopularScores.length的值。 如果要链接所有内容,则需要其他方法来查找数组中元素的数量。 数组中元素的数量在计算平均值时起除数的作用。 让我们看看是否可以更改解决问题的方法,以便可以通过将方法调用组合在链中来完成所有工作。 我们将通过遍历数组元素时跟踪两个值来做到这一点,即使用“双电池”。

 //   // --------------------------------------------------------------------------------- function isFound(item) {    return item.found; }; function getPopularity(item) {    return item.popularity; } //    ,  return,   . 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); 

在这里,为了使用两个值,我们在reducer函数中使用了对象。 每次使用addScrores执行数组addScrores ,我们都会更新人气等级的总值和元素数。 重要的是要注意,这两个值表示为单个对象。 通过这种方法,我们可以“欺骗”系统并将两个实体存储在相同的返回值内。

addScrores函数比上一个示例中具有相同名称的函数要复杂一些。 但是现在事实证明,我们可以使用一个方法调用链来对数组执行所有操作。 处理数组的结果是,我们获得了popularityInfo对象,该对象存储了查找平均值所需的所有内容。 这使呼叫链变得简洁明了。

如果您希望改进此代码,则可以尝试一下。 例如-您可以重做它以摆脱许多中间变量。 您甚至可以尝试将此代码放在一行上。

4.不使用点符号的功能组合


如果您不熟悉函数式编程,或者您觉得函数式编程过于复杂,则可以跳过本节。 如果您已经熟悉curry()compose()则对其进行解析将使您受益。 如果您想深入研究本主题,请阅读有关JavaScript中的函数式编程的资料,尤其是其中包含该资料的系列文章的第三部分。

我们是采用功能性方法的程序员。 这意味着我们努力从其他功能(小型和简单)构建复杂的功能。 到目前为止,在考虑各种解决方案的过程中,我们减少了中间变量的数量。 结果,解决方案代码变得越来越简单。 但是,如果将这个想法发挥到极致呢? 如果您尝试摆脱所有中间变量该怎么办? 甚至尝试摆脱某些参数?

您可以创建一个函数来单独使用compose()函数来计算平均值,而无需使用变量。 我们称其为“不使用细化符号的编程”或“隐式编程”。 为了编写这样的程序,您将需要许多辅助功能。

有时这样的代码会让人震惊。 这是由于这样一种事实,即这种方法与公认的方法有很大不同。 但是我发现以隐式编程的方式编写代码是理解函数式编程本质的最快方法之一。 因此,我建议您在某些个人项目中尝试这种技术。 但是我想说的是,也许您不应该以隐式编程的方式编写其他人必须阅读的代码。

因此,回到我们构建一个计算平均值系统的任务。 为了节省空间,我们将在这里转到使用箭头功能。 通常,通常最好使用命名函数。 这是一篇关于该主题好文章。 如果出现错误,这可以使您获得更好的堆栈跟踪结果。

 //   // ---------------------------------------------------------------------------- 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); //  -   "blackbird combinator". //     : https://jrsinclair.com/articles/2019/compose-js-functions-multiple-parameters/ const B1 = f => g => h => x => f(g(x))(h(x)); //  // ---------------------------------------------------------------------------- //   sum,    . 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()函数中。 如果从下至上读取其内容,结果表明,计算是通过根据found元素的属性过滤数组开始的。 然后,我们使用map()检索popularity元素属性。 之后,我们使用所谓的“ 黑鸟组合器 ”。 该实体表示为函数B1 ,用于对一组输入数据执行两次计算。 为了更好地理解这一点,请看以下示例:

 //   ,  , : 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; 

同样,如果您什么都不懂,请放心。 这只是说明可以用非常不同的方式编写JavaScript。 在这些功能中,这就是这种语言的美。

5.通过累计平均值的计算一次性解决问题


以上所有软件构造都很好地解决了我们的问题(包括命令周期)。 那些使用.reduce()方法的工具有一些共同点。 它们是基于将问题分解为小片段的。 然后以各种方式组装这些片段。 通过分析这些解决方案,您可能会注意到在其中我们进行了三次遍历。 感觉它是无效的。 如果有一种方法可以处理数组并一次性返回结果,那就太好了。 虽然存在此方法,但是其应用将需要求助于数学。

为了一次计算数组元素的平均值,我们需要一种新方法。 您需要找到一种使用先前计算的平均值和新值来计算平均值的方法。 我们使用代数寻找这种方法。

使用以下公式可以找到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); 

多亏了这种方法,才可以绕过数组一次找到必要的值。 其他方法使用一遍过滤数组,另一遍从数组中提取必要的数据,另一遍查找元素值的总和。 在这里,一切都适合一次通过数组。

请注意,这不一定会使计算效率更高。 使用这种方法,必须进行更多的计算。 当每个新值到达时,我们执行乘法和除法运算,以将当前平均值保持在当前状态。 在该问题的其他解决方案中,我们在程序结束时仅将一个数除以另一个。 但是,这种方法在内存使用方面效率更高。 此处不使用中间数组,因此我们只能在内存中存储具有两个值的对象。

. . , . . , , .

?


? , . , - . , , , . , . , . , , .

, - , . , ? . . — .


:

  1. .reduce() .
  2. .filter() .map() , — .reduce() .
  3. , .
  4. .
  5. .

, -, ? — . - — , :

  1. , . — .
  2. , , — .
  3. , , — , .

! JavaScript-?

Source: https://habr.com/ru/post/zh-CN458030/


All Articles