JavaScript reducer - 一个简单而又强大的数组方法

209 阅读7分钟

JavaScript reducer - 一个简单而又强大的数组方法

了解这个基本的JavaScript数组函数的内涵和外延

JavaScript reducer是最有用的数组方法之一,应该放在开发者的武库中。它是在ES5中引入的,与用于数组的for...each和map方法有些类似,但在特定情况下,它的性能和简单性都有所提高。

reduce方法对存储在数组中的每个元素执行我们提供的回调函数,并输出该操作产生的最终值。这是一种更简洁的迭代和处理存储在数组中的数据的方式。

目前,它被所有主要的浏览器版本所支持,并且在Node.js中从10.0版本开始可用。

今天,我们将探讨这种减少方法。我们将通过在不同场景下使用Javascript还原器的方法和时间指南。

那我们就开始吧!

Javascript reduce方法的参数

reduce方法接受两个参数:一个是作为回调的数组的reducer函数,另一个是可选的initialValue 参数。reducer函数需要四个参数。accumulator,currentValue,currentIndex, 和array

arr.reduce(callback( accumulator, currentValue, [, index[, array]] )[, initialValue])

一个Javascript数组还原方法的实例:

[0, 1, 2, 3, 4].reduce((accumulator, currentValue, currentIndex, array) => accumulator + currentValue);

这个reduce方法与下面的for...each循环做了同样的工作,但代码行数较少:

const array = [0, 1, 2, 3, 4];

let total = 0
array.forEach(num => {
  total += num
});

reduce方法是如何使用这些参数实现的呢?

由reduction函数返回的值被分配到accumulator 变量。在每次通过数组项目的迭代中,累积器的值被更新为返回的结果。在迭代结束时,累加器的最终值作为reduce 函数的输出被返回。

如果传递了一个initialValue 参数,在第一次执行还原器函数时,accumulator 将等于initialValuecurrentValue 将等于存储在数组中的第一个元素。如果没有传递初始值,accumulator 将等于数组的第一个值,currentValue 将等于第二个值。

让我们看看在下面的例子中,每次回调函数被调用时,这些参数的值是如何变化的。在这里,我们没有传递一个initialValue 参数:

[0, 1, 2, 3, 4].reduce((accumulator, currentValue, currentIndex, array) => accumulator + currentValue);
累积器currentValuecurrentIndex数组返回值
1010[0, 1, 2, 3, 4]1
2121[0, 1, 2, 3, 4]3
3332[0, 1, 2, 3, 4]6
4643[0, 1, 2, 3, 4]10

这个函数的最终输出是10

接下来,让我们看看当传递一个initialValue ,它是如何工作的:

[0, 1, 2, 3, 4].reduce((accumulator, currentValue, currentIndex, array) => accumulator + currentValue, 12);
累加器currentValuecurrentIndex数组返回值
11200[0, 1, 2, 3, 4]12
21211[0, 1, 2, 3, 4]13
31322[0, 1, 2, 3, 4]15
41533[0, 1, 2, 3, 4]18
51844[0, 1, 2, 3, 4]22

这个函数输出的值是22

什么时候使用Javascript reducer

Reduce方法提供了一种独特的方式来迭代数组中的项目并处理它们。那么,在什么情况下我们可以从这种唯一性中获益呢?

计算一个数组中的数值之和

这与我们在前面的例子中的做法类似。唯一的区别是我们必须为initialValue 参数传递0:

let total = [34, 12, 143, 13, 76].reduce((accumulator, currentValue) => accumulator + currentValue, 0);

扁平化一个数组

如果我们有一个数组,我们可以使用reduce方法将其扁平化,创建一个没有嵌套数组的单一数组:

let array = [[0, 1], [2, 3], [4, 5], [5, 6]];

let flattenedArray = array.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);

我们传递一个空数组作为初始值,这样第一个数组中的项目就会和它连接起来,创建一个扁平化的数组。

如果第一个数组有超过一层的嵌套数组,我们可以递归地调用reduce函数进行扁平化,然后将它们与最终的数组连接起来:

let nestedArray = [[1, [2, 3]], [4, 5], [[6, 7], [8, 9]]];

function flattenArray(nestedArray) {
  return nestedArray.reduce(
    (accumulator, currentValue) => 
      accumulator.concat(
        Array.isArray(currentValue) ? flattenArray(currentValue) : currentValue
      ),
    []);
}

let flattenedArray = flattenArray(nestedArray);

如果回调接受的当前值是一个数组,通过isArray方法验证,我们就递归地对其调用flattenArray函数。如果当前值不是一个数组,我们就简单地将该值与最终的扁平化数组连接起来。

通过一个属性对一个数组的对象进行分组

假设我们有一个国家对象的数组。我们想根据大洲来分组数组中的每个国家。我们可以使用reduce方法来完成这个任务:

let countries = [
    {name: "Germany", continent: "Europe"},
    {name: "Brazil", continent: "South America"},
    {name: "India", continent: "Asia"},
    {name: "France", continent: "Europe"},
    {name: "South Korea", continent: "Asia"},
]

let groupedCountries = countries.reduce(
  (groupedCountries, country) => {
    if (!groupedCountries[country.continent]){
      groupedCountries[country.continent] = []
    }
    groupedCountries[country.continent].push(country)
    return groupedCountries
  },
  {});

在回调函数中,我们为不在groupedCountries地图中的每个大陆创建一个新的键,并分配一个空数组作为其值。然后,我们将每个国家对象推送到由其各自大陆存储的数组中。

使用reduce()来代替filter().map()。

在Javascript中,我们使用过滤器方法来过滤存储在数组中的项目,使用回调。我们使用map方法,使用回调里面传递的逻辑,用旧数组创建一个新数组。有时,我们必须使用这两个方法,一个接一个地创建一个新的数组,其中包含我们使用某些条件过滤的结果。

与其使用两个数组方法,你可以使用Javascript数组减少方法来完成同样的任务。这将减少完成时间,因为现在你只需在数组中迭代一次,而不是两次。

例如,让我们来看看下面这个场景,我们想创建一个大于30的数字的平方根数组:

let numbers = [3, 21, 34, 121, 553, 12, 53, 5, 42, 11];

let newArray = numbers.filter(number => number > 30).map(number => Math.sqrt(number));

使用reduce实现的相同场景看起来是这样的:

let numbers = [3, 21, 34, 121, 553, 12, 53, 5, 42, 11];

let newArray = numbers.reduce((accumulator, current) => {
  if (current > 30) {
    accumulator.push(Math.sqrt(current))
  }
  return accumulator
}, []);

在回调中,我们简单地检查数字是否大于30,并将其平方根添加到累积器数组中。你必须传递一个空数组作为初始值才能得到这个结果。

建立你自己的还原器

在这一节中,我们将自己实现减速器函数,看看事情是如何在引擎盖下工作的。这将使你更好地了解在你的程序中何时使用Javascript reducer以获得最佳性能:

Array.prototype.reduceConceptual = function(callback, initial) {
  if (!this) {
    throw Error('Array.prototype.forEach called on null or undefined');
  }

  if (!callback || typeof callback !== 'function') {
    throw Error('callback is not a function');
  }

  let acc = initial;

  for(i=0;i<this.length;i++) {
    acc = callback(acc, this[i], i, this);
  }

  return acc;
}

首先,我们检查reduce方法是否是在一个空对象或未定义对象上调用的。然后,我们检查传递的回调是否是一个函数。

在初始类型检查之后,我们将传递的初始值分配给累积器。然后我们在数组中循环,为数组中的每一项调用回调。在执行结束时,我们必须返回累加器的值。

我们使用这个实现只是为了帮助你理解reduce方法的实际工作原理。例如,你可以看到它使用了一个for循环来遍历数组的外壳。

然而,请注意,你不应该在你的生产代码中使用这个实现。事实上,将方法原型化为Javascript标准类型是一种糟糕的编码实践,你不应该沉迷其中。

总结

在这篇文章中,我们讨论了最有用的Javascript数组方法之一,Javascript reducer。我们讨论了如何以及何时使用它,甚至从头开始建立我们自己的reduce方法。

我希望这些知识能够帮助你在将来发现可以通过使用reducer来解决的问题。其中一些用例与forEach,map, 和filter 数组方法重叠。所以你应该知道挑选那些可以用reduce方法优化解决的情况。