Array.prototype.reduce()

基线 广泛可用

此功能已得到良好建立,并且可在许多设备和浏览器版本上运行。它自以下时间起在所有浏览器中都可用 2015 年 7 月.

reduce() 方法是 Array 实例的方法,它按顺序对数组中的每个元素执行用户提供的“reducer”回调函数,并将前一个元素计算的返回值传递给它。在所有数组元素上运行 reducer 的最终结果是一个单一的值。

回调函数第一次运行时,没有“前一个计算的返回值”。如果提供了初始值,则可以使用它来代替。否则,数组中索引为 0 的元素将用作初始值,并且迭代将从下一个元素(索引 1 而不是索引 0)开始。

试一试

语法

js
reduce(callbackFn)
reduce(callbackFn, initialValue)

参数

callbackFn

一个为数组中的每个元素执行的函数。它的返回值成为 callbackFn 下一次调用时 accumulator 参数的值。对于最后一次调用,返回值将成为 reduce() 的返回值。该函数使用以下参数调用

accumulator

先前对 callbackFn 的调用产生的值。在第一次调用时,如果指定了 initialValue,则其值为 initialValue;否则,其值为 array[0]

currentValue

当前元素的值。在第一次调用时,如果指定了 initialValue,则其值为 array[0];否则,其值为 array[1]

currentIndex

currentValue 在数组中的索引位置。在第一次调用时,如果指定了 initialValue,则其值为 0,否则为 1

array

调用 reduce() 的数组。

initialValue 可选

accumulator 在回调函数第一次被调用时初始化的值。如果指定了 initialValue,则 callbackFn 将以数组中的第一个值作为 currentValue 开始执行。如果指定 initialValue,则 accumulator 将初始化为数组中的第一个值,并且 callbackFn 将以数组中的第二个值作为 currentValue 开始执行。在这种情况下,如果数组为空(因此没有第一个值可以作为 accumulator 返回),则会抛出错误。

返回值

在整个数组上完成“reducer”回调函数运行后产生的值。

异常

TypeError

如果数组不包含任何元素且未提供 initialValue,则抛出。

描述

reduce() 方法是 迭代方法。它按升序索引顺序对数组中的所有元素运行“reducer”回调函数,并将它们累积到一个单一的值。每次,callbackFn 的返回值都会在下次调用时作为 accumulator 再次传递给 callbackFnaccumulator 的最终值(即在数组的最后一次迭代中从 callbackFn 返回的值)将成为 reduce() 的返回值。阅读 迭代方法 部分,以获取有关这些方法如何普遍工作的更多信息。

callbackFn 仅对已分配值的数组索引调用。它不会对 稀疏数组 中的空槽调用。

与其他 迭代方法 不同,reduce() 不接受 thisArg 参数。callbackFn 始终以 undefined 作为 this 调用,如果 callbackFn 非严格,则将其替换为 globalThis

reduce()函数式编程 中的核心概念,在函数式编程中,不可能改变任何值,因此为了累积数组中的所有值,必须在每次迭代中返回一个新的累加器值。此约定传播到 JavaScript 的 reduce():您应该尽可能使用 扩展运算符 或其他复制方法来创建新的数组和对象作为累加器,而不是修改现有的累加器。如果您决定修改累加器而不是复制它,请记住在回调函数中仍然返回修改后的对象,否则下一次迭代将接收 undefined。但是,请注意,复制累加器可能会导致内存使用量增加和性能下降——有关更多详细信息,请参阅 何时不使用 reduce()。在这种情况下,为了避免性能不佳和代码难以阅读,最好使用简单的 for 循环。

reduce() 方法是 泛型 的。它只期望 this 值具有 length 属性和整数键属性。

边缘情况

如果数组只有一个元素(无论位置如何)并且没有提供 initialValue,或者如果提供了 initialValue 但数组为空,则会调用 callbackFn 即可返回单个值。

如果提供了 initialValue 并且数组不为空,则 reduce 方法将始终从索引 0 开始调用回调函数。

如果未提供 initialValue,则 reduce 方法对于长度大于 1、等于 1 和 0 的数组的行为将有所不同,如下例所示

js
const getMax = (a, b) => Math.max(a, b);

// callback is invoked for each element in the array starting at index 0
[1, 100].reduce(getMax, 50); // 100
[50].reduce(getMax, 10); // 50

// callback is invoked once for element at index 1
[1, 100].reduce(getMax); // 100

// callback is not invoked
[50].reduce(getMax); // 50
[].reduce(getMax, 1); // 1

[].reduce(getMax); // TypeError

示例

reduce() 在没有初始值的情况下如何工作

以下代码显示了如果我们使用数组和没有初始值调用 reduce() 会发生什么。

js
const array = [15, 16, 17, 18, 19];

function reducer(accumulator, currentValue, index) {
  const returns = accumulator + currentValue;
  console.log(
    `accumulator: ${accumulator}, currentValue: ${currentValue}, index: ${index}, returns: ${returns}`,
  );
  return returns;
}

array.reduce(reducer);

回调函数将被调用四次,每次调用的参数和返回值如下所示

accumulator currentValue 索引 返回值
第一次调用 15 16 1 31
第二次调用 31 17 2 48
第三次调用 48 18 3 66
第四次调用 66 19 4 85

array 参数在整个过程中从未改变——它始终为 [15, 16, 17, 18, 19]reduce() 返回的值将是最后一次回调调用的值 (85)。

reduce() 函数如何使用初始值

这里我们使用相同的算法对同一个数组进行 reduce 操作,但将 initialValue 设置为 10,作为 reduce() 的第二个参数传递。

js
[15, 16, 17, 18, 19].reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  10,
);

回调函数将被调用五次,每次调用的参数和返回值如下所示

accumulator currentValue 索引 返回值
第一次调用 10 15 0 25
第二次调用 25 16 1 41
第三次调用 41 17 2 58
第四次调用 58 18 3 76
第五次调用 76 19 4 95

在这种情况下,reduce() 返回的值将是 95

对象数组中值的总和

为了对包含在对象数组中的值求和,您**必须**提供一个 initialValue,以便每个项目都通过您的函数。

js
const objects = [{ x: 1 }, { x: 2 }, { x: 3 }];
const sum = objects.reduce(
  (accumulator, currentValue) => accumulator + currentValue.x,
  0,
);

console.log(sum); // 6

函数顺序管道

pipe 函数接收一系列函数并返回一个新函数。当使用参数调用新函数时,这些函数将按顺序调用,每个函数接收前一个函数的返回值。

js
const pipe =
  (...functions) =>
  (initialValue) =>
    functions.reduce((acc, fn) => fn(acc), initialValue);

// Building blocks to use for composition
const double = (x) => 2 * x;
const triple = (x) => 3 * x;
const quadruple = (x) => 4 * x;

// Composed functions for multiplication of specific values
const multiply6 = pipe(double, triple);
const multiply9 = pipe(triple, triple);
const multiply16 = pipe(quadruple, quadruple);
const multiply24 = pipe(double, triple, quadruple);

// Usage
multiply6(6); // 36
multiply9(9); // 81
multiply16(16); // 256
multiply24(10); // 240

顺序运行 Promise

Promise 顺序执行 本质上就是上一节中演示的函数管道,只不过是异步执行的。

js
// Compare this with pipe: fn(acc) is changed to acc.then(fn),
// and initialValue is ensured to be a promise
const asyncPipe =
  (...functions) =>
  (initialValue) =>
    functions.reduce((acc, fn) => acc.then(fn), Promise.resolve(initialValue));

// Building blocks to use for composition
const p1 = async (a) => a * 5;
const p2 = async (a) => a * 2;
// The composed functions can also return non-promises, because the values are
// all eventually wrapped in promises
const f3 = (a) => a * 3;
const p4 = async (a) => a * 4;

asyncPipe(p1, p2, f3, p4)(10).then(console.log); // 1200

asyncPipe 也可以使用 async/await 实现,这更好地展示了它与 pipe 的相似性。

js
const asyncPipe =
  (...functions) =>
  (initialValue) =>
    functions.reduce(async (acc, fn) => fn(await acc), initialValue);

对稀疏数组使用 reduce()

reduce() 跳过稀疏数组中的缺失元素,但它不会跳过 undefined 值。

js
console.log([1, 2, , 4].reduce((a, b) => a + b)); // 7
console.log([1, 2, undefined, 4].reduce((a, b) => a + b)); // NaN

在非数组对象上调用 reduce()

reduce() 方法读取 thislength 属性,然后访问每个键为小于 length 的非负整数的属性。

js
const arrayLike = {
  length: 3,
  0: 2,
  1: 3,
  2: 4,
  3: 99, // ignored by reduce() since length is 3
};
console.log(Array.prototype.reduce.call(arrayLike, (x, y) => x + y));
// 9

何时不使用 reduce()

reduce() 这样的多用途高阶函数功能强大,但有时难以理解,尤其对于经验不足的 JavaScript 开发人员而言。如果使用其他数组方法使代码更清晰,开发人员必须权衡可读性与使用 reduce() 的其他优势。

请注意,reduce() 始终等效于 for...of 循环,只是我们不再修改上层作用域中的变量,而是为每次迭代返回新值。

js
const val = array.reduce((acc, cur) => update(acc, cur), initialValue);

// Is equivalent to:
let val = initialValue;
for (const cur of array) {
  val = update(val, cur);
}

如前所述,人们可能想要使用 reduce() 的原因是为了模仿函数式编程中不变数据的实践。因此,坚持累加器不变性的开发人员通常会为每次迭代复制整个累加器,如下所示

js
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = names.reduce((allNames, name) => {
  const currCount = Object.hasOwn(allNames, name) ? allNames[name] : 0;
  return {
    ...allNames,
    [name]: currCount + 1,
  };
}, {});

此代码性能不佳,因为每次迭代都必须复制整个 allNames 对象,而这可能很大,具体取决于有多少唯一名称。此代码的最坏情况性能为 O(N^2),其中 Nnames 的长度。

更好的替代方法是在每次迭代中修改 allNames 对象。但是,如果无论如何都要修改 allNames,则可能希望将 reduce() 转换为简单的 for 循环,这样更清晰。

js
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = names.reduce((allNames, name) => {
  const currCount = allNames[name] ?? 0;
  allNames[name] = currCount + 1;
  // return allNames, otherwise the next iteration receives undefined
  return allNames;
}, Object.create(null));
js
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = Object.create(null);
for (const name of names) {
  const currCount = countedNames[name] ?? 0;
  countedNames[name] = currCount + 1;
}

因此,如果您的累加器是数组或对象,并且您在每次迭代中复制数组或对象,则可能会意外地将二次复杂度引入代码,导致大型数据的性能迅速下降。这在实际代码中确实发生过——例如,请参见 通过一行代码更改使 Tanstack Table 速度提高 1000 倍

上面给出了一些 reduce() 的可接受用例(最值得注意的是,对数组求和、Promise 顺序执行和函数管道)。在其他情况下,存在比 reduce() 更好的替代方法。

  • 展平数组的数组。请改用 flat()
    js
    const flattened = array.reduce((acc, cur) => acc.concat(cur), []);
    
    js
    const flattened = array.flat();
    
  • 按属性对对象进行分组。请改用 Object.groupBy()
    js
    const groups = array.reduce((acc, obj) => {
      const key = obj.name;
      const curGroup = acc[key] ?? [];
      return { ...acc, [key]: [...curGroup, obj] };
    }, {});
    
    js
    const groups = Object.groupBy(array, (obj) => obj.name);
    
  • 连接对象数组中包含的数组。请改用 flatMap()
    js
    const friends = [
      { name: "Anna", books: ["Bible", "Harry Potter"] },
      { name: "Bob", books: ["War and peace", "Romeo and Juliet"] },
      { name: "Alice", books: ["The Lord of the Rings", "The Shining"] },
    ];
    const allBooks = friends.reduce((acc, cur) => [...acc, ...cur.books], []);
    
    js
    const allBooks = friends.flatMap((person) => person.books);
    
  • 删除数组中的重复项。请改用 SetArray.from()
    js
    const uniqArray = array.reduce(
      (acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]),
      [],
    );
    
    js
    const uniqArray = Array.from(new Set(array));
    
  • 删除或添加数组中的元素。请改用 flatMap()
    js
    // Takes an array of numbers and splits perfect squares into its square roots
    const roots = array.reduce((acc, cur) => {
      if (cur < 0) return acc;
      const root = Math.sqrt(cur);
      if (Number.isInteger(root)) return [...acc, root, root];
      return [...acc, cur];
    }, []);
    
    js
    const roots = array.flatMap((val) => {
      if (val < 0) return [];
      const root = Math.sqrt(val);
      if (Number.isInteger(root)) return [root, root];
      return [val];
    });
    
    如果仅从数组中删除元素,也可以使用 filter()
  • 搜索元素或测试元素是否满足条件。请改用 find()findIndex(),或 some()every()。这些方法的额外好处是,它们在结果确定后立即返回,而无需遍历整个数组。
    js
    const allEven = array.reduce((acc, cur) => acc && cur % 2 === 0, true);
    
    js
    const allEven = array.every((val) => val % 2 === 0);
    

reduce() 是最佳选择的情况下,文档和语义变量命名可以帮助缓解可读性问题。

规范

规范
ECMAScript 语言规范
# sec-array.prototype.reduce

浏览器兼容性

BCD 表格仅在启用 JavaScript 的浏览器中加载。

另请参阅