处理数组的三种方法介绍:for-of vs. .reduce() vs. .flatMap()

83 阅读9分钟

在这篇博文中,我们看一下处理Arrays的三种方法:

  • for-of 循环
  • 数组方法.reduce()
  • 数组方法.flatMap()

我们的目标是帮助你在需要处理数组时在这些功能之间做出选择。如果你还不知道.reduce().flatMap() ,我们将向你解释这两种方法。

为了更好地感受这三个特性的工作原理,我们分别使用它们来实现以下功能。

  • 过滤一个输入数组以产生一个输出数组
  • 将每个输入数组元素映射为一个输出数组元素
  • 将每个输入数组元素扩展为零个或多个输出数组元素
  • 筛选-映射(筛选和映射在一个步骤中)。
  • 计算一个数组的摘要
  • 寻找一个数组元素
  • 检查所有Array元素的一个条件

我们所做的一切都是非破坏性的。输入的数组永远不会被改变。如果输出是一个数组,它始终是新创建的。


通过for-of 循环处理数组

这就是如何通过for-of 对数组进行非破坏性的转换:

  • 首先声明变量result ,并用一个空的数组来初始化它。
  • 对于输入数组的每个元素elem
    • 如果一个值应该被添加到result
      • 根据需要转换elem ,并将其推入result

for-of 进行过滤

让我们通过for-of 来感受一下处理数组,并实现(简化版的)数组方法.filter()

function filterArray(arr, callback) {
  const result = [];
  for (const elem of arr) {
    if (callback(elem)) {
      result.push(elem);
    }
  }
  return result;
}

assert.deepEqual(
  filterArray(['', 'a', '', 'b'], str => str.length > 0),
  ['a', 'b']
);

for-of 进行映射

我们还可以使用for-of 来实现数组方法.map()

function mapArray(arr, callback) {
  const result = [];
  for (const elem of arr) {
    result.push(callback(elem));
  }
  return result;
}

assert.deepEqual(
  mapArray(['a', 'b', 'c'], str => str + str),
  ['aa', 'bb', 'cc']
);

for-of 进行扩展

collectFruits() 返回数组中的人所拥有的所有水果。

function collectFruits(persons) {
  const result = [];
  for (const person of persons) {
    result.push(...person.fruits);
  }
  return result;
}

const PERSONS = [  {    name: 'Jane',    fruits: ['strawberry', 'raspberry'],
  },
  {
    name: 'John',
    fruits: ['apple', 'banana', 'orange'],
  },
  {
    name: 'Rex',
    fruits: ['melon'],
  },
];
assert.deepEqual(
  collectFruits(PERSONS),
  ['strawberry', 'raspberry', 'apple', 'banana', 'orange', 'melon']
);

for-of 进行过滤映射

下面的代码在一个步骤中过滤和映射:

  • 过滤是通过A行的if 语句和B行的.push() 方法完成的。
  • 映射是通过推送movie.title (不是输入元素movie )完成的。

for-of 计算一个摘要

getAverageGrade() 计算一个数组的学生的平均成绩。

function getAverageGrade(students) {
  let sumOfGrades = 0;
  for (const student of students) {
    sumOfGrades += student.grade;
  }
  return sumOfGrades / students.length;
}

const STUDENTS = [
  {
    id: 'qk4k4yif4a',
    grade: 4.0,
  },
  {
    id: 'r6vczv0ds3',
    grade: 0.25,
  },
  {
    id: '9s53dn6pbk',
    grade: 1,
  },
];
assert.equal(
  getAverageGrade(STUDENTS),
  1.75
);

注意:用小数点计算会导致四舍五入的错误(更多信息)。

for-of 查找

for-of 也善于在未排序的数组中寻找东西。

在这里,我们可以通过return ,一旦我们找到了东西(A行),就可以提前离开循环。

for-of 检查一个条件

当实现数组方法.every() ,我们又一次从早期的循环终止中获益(A行)。

何时使用for-of

for-of 在处理数组时,是一个非常通用的工具。

  • 通过推送创建输出数组很容易理解。
  • 当结果不是数组时,我们可以通过returnbreak 提前结束,这往往很有用。

for-of 的一个缺点是,它可能比其他方法更啰嗦--这取决于我们要解决什么问题。

生成器和for-of

yield 在上一节中已经提到了,但我还想指出生成器对于处理和产生同步异步的可迭代数据是多么的方便--认为流是按需处理流项目的。

作为例子,让我们通过同步生成器实现.filter().map()

function* filterIterable(iterable, callback) {
  for (const item of iterable) {
    if (callback(item)) {
      yield item;
    }
  }
}
const iterable1 = filterIterable(
  ['', 'a', '', 'b'],
  str => str.length > 0
);
assert.deepEqual(
  Array.from(iterable1),
  ['a', 'b']
);

function* mapIterable(iterable, callback) {
  for (const item of iterable) {
    yield callback(item);
  }
}
const iterable2 = mapIterable(['a', 'b', 'c'], str => str + str);
assert.deepEqual(
  Array.from(iterable2),
  ['aa', 'bb', 'cc']
);

数组方法.reduce()

在我们讨论.reduce() 本身之前,让我们通过for-of 实现其算法。我们将使用串联一个字符串数组作为例子。

Array方法.reduce() 循环,并为我们保持对摘要的跟踪,这样我们就可以专注于初始化和更新。它使用 "累加器 "这个名字作为 "摘要 "的粗略同义词。.reduce() 有两个参数:

  1. 一个回调。
    • 输入:旧累加器和当前元素
    • 输出:新的累加器
  2. 累加器的初始值。

在下面的代码中,我们用.reduce() 来实现concatElements()

.reduce() 进行过滤

.reduce() 是相当通用的。让我们用它来实现过滤。

const filterArray = (arr, callback) => arr.reduce(
  (acc, elem) => callback(elem) ? [...acc, elem] : acc,
  []
);
assert.deepEqual(
  filterArray(['', 'a', '', 'b'], str => str.length > 0),
  ['a', 'b']
);

唉,当涉及到非破坏性地添加元素到Arrays时,JavaScript Arrays的效率并不高(与许多函数式编程语言中的链接列表相比)。因此,突变累加器的效率更高。

const filterArray = (arr, callback) => arr.reduce(
  (acc, elem) => {
    if (callback(elem)) {
      acc.push(elem);
    }
    return acc;
  },
  []
);

.reduce() 进行映射

我们可以通过.reduce() 进行映射,如下所示。

const mapArray = (arr, callback) => arr.reduce(
  (acc, elem) => [...acc, callback(elem)],
  []
);
assert.deepEqual(
  mapArray(['a', 'b', 'c'], str => str + str),
  ['aa', 'bb', 'cc']
);

突变的版本也是更有效的。

const mapArray = (arr, callback) => arr.reduce(
  (acc, elem) => {
    acc.push(callback(elem));
    return acc;
  },
  []
);

.reduce() 进行扩展

.reduce() 进行扩展。

const collectFruits = (persons) => persons.reduce(
  (acc, person) => [...acc, ...person.fruits],
  []
);

const PERSONS = [  {    name: 'Jane',    fruits: ['strawberry', 'raspberry'],
  },
  {
    name: 'John',
    fruits: ['apple', 'banana', 'orange'],
  },
  {
    name: 'Rex',
    fruits: ['melon'],
  },
];
assert.deepEqual(
  collectFruits(PERSONS),
  ['strawberry', 'raspberry', 'apple', 'banana', 'orange', 'melon']
);

突变的版本:

const collectFruits = (persons) => persons.reduce(
  (acc, person) => {
    acc.push(...person.fruits);
    return acc;
  },
  []
);

.reduce() 进行过滤映射

使用.reduce() ,在一个步骤中进行过滤和映射:

const getTitles = (movies, minRating) => movies.reduce(
  (acc, movie) => (movie.rating >= minRating)
    ? [...acc, movie.title]
    : acc,
  []
);

const MOVIES = [
  { title: 'Inception', rating: 8.8 },
  { title: 'Arrival', rating: 7.9 },
  { title: 'Groundhog Day', rating: 8.1 },
  { title: 'Back to the Future', rating: 8.5 },
  { title: 'Being John Malkovich', rating: 7.8 },
];
assert.deepEqual(
  getTitles(MOVIES, 8),
  ['Inception', 'Groundhog Day', 'Back to the Future']
);

更有效的变异版本:

const getTitles = (movies, minRating) => movies.reduce(
  (acc, movie) => {
    if (movie.rating >= minRating) {
      acc.push(movie.title);
    }
    return acc;
  },
  []
);

.reduce() 计算摘要

.reduce() 如果我们能在不改变累加器的情况下有效地计算出一个总结,那么就会很出色。

const getAverageGrade = (students) => {
  const sumOfGrades = students.reduce(
    (acc, student) => acc + student.grade,
    0
  );
  return sumOfGrades  / students.length;
};

const STUDENTS = [
  {
    id: 'qk4k4yif4a',
    grade: 4.0,
  },
  {
    id: 'r6vczv0ds3',
    grade: 0.25,
  },
  {
    id: '9s53dn6pbk',
    grade: 1,
  },
];
assert.equal(
  getAverageGrade(STUDENTS),
  1.75
);

注意事项:用小数点计算会导致四舍五入的错误(更多信息)。

.reduce() 查找

这是(简化版的)阵列方法.find() ,用.reduce() 实现。

const findInArray = (arr, callback) => arr.reduce(
  (acc, value, index) => (acc === undefined && callback(value))
    ? {index, value}
    : acc,
  undefined
);

assert.deepEqual(
  findInArray(['', 'a', '', 'b'], str => str.length > 0),
  {index: 1, value: 'a'}
);
assert.deepEqual(
  findInArray(['', 'a', '', 'b'], str => str.length > 1),
  undefined
);

.reduce() 的一个限制在这里是相关的。一旦我们找到一个值,我们仍然要访问剩余的元素,因为我们不能提前退出。for-of 没有这个限制。

.reduce() 检查一个条件

这是(简化版的)Array方法.every() ,用.reduce() 实现。

const everyArrayElement = (arr, condition) => arr.reduce(
  (acc, elem) => !acc ? acc : condition(elem),
  true
);

assert.equal(
  everyArrayElement(['a', '', 'b'], str => str.length > 0),
  false
);
assert.equal(
  everyArrayElement(['a', 'b'], str => str.length > 0),
  true
);

同样,如果我们可以提前退出.reduce() ,这个实现会更有效率。

什么时候使用.reduce()

.reduce() 的优点是它的简洁性。缺点是它可能难以理解--特别是当你不习惯于函数式编程时。

我使用.reduce() ,如果:

  • 我不需要对累加器进行变异。
  • 我不需要提前退出。
  • 我不需要对同步或异步迭代的支持。

.reduce() 当一个摘要(如所有元素的总和)可以在没有变异的情况下被计算时,"累积器 "是一个很好的工具。

可惜的是,JavaScript并不擅长以非破坏性和增量的方式创建Arrays。这就是为什么我在JavaScript中较少使用.reduce() ,而在那些有内置不可变列表的语言中则较少使用相应的操作。

阵列方法.flatMap()

普通的.map() 方法将每个输入元素精确地翻译成一个输出元素。

相比之下,.flatMap() 可以将每个输入元素翻译成零个或多个输出元素。为了达到这个目的,回调并不返回值,而是返回值的数组。

assert.equal(
  [0, 1, 2, 3].flatMap(num => new Array(num).fill(String(num))),
  ['1', '2', '2', '3', '3', '3']
);

.flatMap() 进行过滤

这就是我们如何用.flatMap() 进行过滤。

const filterArray = (arr, callback) => arr.flatMap(
  elem => callback(elem) ? [elem] : []
);

assert.deepEqual(
  filterArray(['', 'a', '', 'b'], str => str.length > 0),
  ['a', 'b']
);

.flatMap() 进行映射

这就是我们如何用.flatMap() 进行映射。

const mapArray = (arr, callback) => arr.flatMap(
  elem => [callback(elem)]
);

assert.deepEqual(
  mapArray(['a', 'b', 'c'], str => str + str),
  ['aa', 'bb', 'cc']
);

.flatMap() 进行过滤-映射

在一个步骤中进行过滤和映射是.flatMap() 的优势之一。

const getTitles = (movies, minRating) => movies.flatMap(
  (movie) => (movie.rating >= minRating) ? [movie.title] : []
);

const MOVIES = [
  { title: 'Inception', rating: 8.8 },
  { title: 'Arrival', rating: 7.9 },
  { title: 'Groundhog Day', rating: 8.1 },
  { title: 'Back to the Future', rating: 8.5 },
  { title: 'Being John Malkovich', rating: 7.8 },
];

assert.deepEqual(
  getTitles(MOVIES, 8),
  ['Inception', 'Groundhog Day', 'Back to the Future']
);

.flatMap() 进行扩展

将输入元素扩展为零或更多的输出元素是.flatMap() 的另一个优势。

const collectFruits = (persons) => persons.flatMap(
  person => person.fruits
);

const PERSONS = [  {    name: 'Jane',    fruits: ['strawberry', 'raspberry'],
  },
  {
    name: 'John',
    fruits: ['apple', 'banana', 'orange'],
  },
  {
    name: 'Rex',
    fruits: ['melon'],
  },
];
assert.deepEqual(
  collectFruits(PERSONS),
  ['strawberry', 'raspberry', 'apple', 'banana', 'orange', 'melon']
);

.flatMap() 只能产生数组

使用.flatMap() ,我们只能产生数组。这使我们不能:

  • 用 "计算总结 "来计算.flatMap()
  • 用 "寻找 "来寻找.flatMap()
  • 用 "检查条件"(Checking a condition).flatMap()

可以想象,我们可以产生一个被数组包裹的值。然而,我们不能在回调的调用之间传递数据。这使得我们无法,例如,跟踪我们是否已经找到了什么。而且我们不能提前退出。

什么时候使用.flatMap()

.flatMap() 是很好的:

  • 同时进行过滤和映射
  • 将输入元素扩展为零或多个输出元素

我还发现它相对容易理解。然而,它不像for-of 和--在较小程度上--.reduce() 那样通用。

  • 它只能产生数组作为结果。
  • 我们不能在回调的调用之间传递数据。
  • 我们不能提前退出。

建议

那么,我们怎样才能最好地使用这些工具来处理数组呢?我的粗略的一般性建议是:

  • 使用你拥有的最具体的工具来完成这个任务。
    • 你需要过滤吗?使用.filter()
    • 你需要映射吗?使用.map()
    • 你需要检查一个元素的条件吗?使用.some().every()
    • 等等。
  • for-of 是最通用的工具。根据我的经验。
    • 熟悉函数式编程的人,倾向于使用.reduce().flatMap()
    • 不熟悉函数式编程的人,通常认为for-of 更容易理解。然而,for-of 通常会导致更多冗长的代码。
  • .reduce() 如果不需要改变累加器的话,它擅长计算摘要(如所有元素的总和)。
  • .flatMap() 擅长过滤映射和将输入元素扩展为零或多个输出元素。