Array.prototype.reduce()

Array 实例的 reduce() 方法按顺序对数组的每个元素执行用户提供的 "reducer" 回调函数,并传入前一个元素计算的返回值。对数组的所有元素运行缩减程序的最终结果是单个值。

¥The reduce() method of Array instances executes a user-supplied "reducer" callback function on each element of the array, in order, passing in the return value from the calculation on the preceding element. The final result of running the reducer across all elements of the array is a single value.

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

¥The first time that the callback is run there is no "return value of the previous calculation". If supplied, an initial value may be used in its place. Otherwise the array element at index 0 is used as the initial value and iteration starts from the next element (index 1 instead of index 0).

Try it

语法

¥Syntax

js
reduce(callbackFn)
reduce(callbackFn, initialValue)

参数

¥Parameters

callbackFn

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

accumulator

上次调用 callbackFn 所产生的值。第一次调用时,如果指定了后者,则其值为 initialValue;否则其值为 array[0]

currentValue

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

currentIndex

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

array

调用了数组 reduce()

initialValue Optional

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

返回值

¥Return value

在整个数组上运行 "reducer" 回调函数直至完成所产生的值。

¥The value that results from running the "reducer" callback function to completion over the entire array.

例外情况

¥Exceptions

TypeError

如果数组不包含元素且未提供 initialValue,则抛出该错误。

描述

¥Description

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

¥The reduce() method is an iterative method. It runs a "reducer" callback function over all elements in the array, in ascending-index order, and accumulates them into a single value. Every time, the return value of callbackFn is passed into callbackFn again on next invocation as accumulator. The final value of accumulator (which is the value returned from callbackFn on the final iteration of the array) becomes the return value of reduce(). Read the iterative methods section for more information about how these methods work in general.

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

¥callbackFn is invoked only for array indexes which have assigned values. It is not invoked for empty slots in sparse arrays.

与其他 迭代法 不同,reduce() 不接受 thisArg 参数。callbackFn 总是用 undefined 作为 this 来调用,如果 callbackFn 是非严格的,则用 globalThis 替换。

¥Unlike other iterative methods, reduce() does not accept a thisArg argument. callbackFn is always called with undefined as this, which gets substituted with globalThis if callbackFn is non-strict.

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

¥reduce() is a central concept in functional programming, where it's not possible to mutate any value, so in order to accumulate all values in an array, one must return a new accumulator value on every iteration. This convention propagates to JavaScript's reduce(): you should use spreading or other copying methods where possible to create new arrays and objects as the accumulator, rather than mutating the existing one. If you decided to mutate the accumulator instead of copying it, remember to still return the modified object in the callback, or the next iteration will receive undefined. However, note that copying the accumulator may in turn lead to increased memory usage and degraded performance — see When to not use reduce() for more details. In such cases, to avoid bad performance and unreadable code, it's better to use a simple for loop instead.

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

¥The reduce() method is generic. It only expects the this value to have a length property and integer-keyed properties.

边缘情况

¥Edge cases

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

¥If the array only has one element (regardless of position) and no initialValue is provided, or if initialValue is provided but the array is empty, the solo value will be returned without calling callbackFn.

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

¥If initialValue is provided and the array is not empty, then the reduce method will always invoke the callback function starting at index 0.

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

¥If initialValue is not provided then the reduce method will act differently for arrays with length larger than 1, equal to 1 and 0, as shown in the following example:

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

示例

¥Examples

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

¥How reduce() works without an initial value

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

¥The code below shows what happens if we call reduce() with an array and no initial value.

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);

该回调将被调用四次,每次调用中的参数和返回值如下:

¥The callback would be invoked four times, with the arguments and return values in each call being as follows:

accumulator currentValue index 返回值
第一次通话 15 16 1 31
第二次通话 31 17 2 48
第三次通话 48 18 3 66
第四次通话 66 19 4 85

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

¥The array parameter never changes through the process — it's always [15, 16, 17, 18, 19]. The value returned by reduce() would be that of the last callback invocation (85).

reduce() 如何使用初始值

¥How reduce() works with an initial value

在这里,我们使用相同的算法减少相同的数组,但将 10initialValue 作为第二个参数传递给 reduce()

¥Here we reduce the same array using the same algorithm, but with an initialValue of 10 passed as the second argument to reduce():

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

该回调将被调用五次,每次调用的参数和返回值如下:

¥The callback would be invoked five times, with the arguments and return values in each call being as follows:

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

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

¥The value returned by reduce() in this case would be 95.

对象数组中值的总和

¥Sum of values in an object array

要对对象数组中包含的值求和,你必须提供 initialValue,以便每个项目都通过你的函数。

¥To sum up the values contained in an array of objects, you must supply an initialValue, so that each item passes through your function.

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

console.log(sum); // 6

函数顺序管道

¥Function sequential piping

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

¥The pipe function takes a sequence of functions and returns a new function. When the new function is called with an argument, the sequence of functions are called in order, which each one receiving the return value of the previous function.

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

¥Running promises in sequence

Promise 排序 本质上是上一节中演示的函数管道,只不过是异步完成的。

¥Promise sequencing is essentially function piping demonstrated in the previous section, except done asynchronously.

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 的相似性:

¥asyncPipe can also be implemented using async/await, which better demonstrates its similarity with pipe:

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

将 reduce() 与稀疏数组结合使用

¥Using reduce() with sparse arrays

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

¥reduce() skips missing elements in sparse arrays, but it does not skip undefined values.

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()

¥Calling reduce() on non-array objects

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

¥The reduce() method reads the length property of this and then accesses each property whose key is a nonnegative integer less than 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()

¥When to not use reduce()

reduce() 这样的多用途高阶函数可能很强大,但有时很难理解,特别是对于经验不足的 JavaScript 开发者来说。如果使用其他数组方法时代码变得更清晰,开发者必须权衡可读性与使用 reduce() 的其他好处。

¥Multipurpose higher-order functions like reduce() can be powerful but sometimes difficult to understand, especially for less-experienced JavaScript developers. If code becomes clearer when using other array methods, developers must weigh the readability tradeoff against the other benefits of using reduce().

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

¥Note that reduce() is always equivalent to a for...of loop, except that instead of mutating a variable in the upper scope, we now return the new value for each iteration:

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() 的原因是模仿不可变数据的函数式编程实践。因此,维护累加器不可变性的开发者通常会在每次迭代时复制整个累加器,如下所示:

¥As previously stated, the reason why people may want to use reduce() is to mimic functional programming practices of immutable data. Therefore, developers who uphold the immutability of the accumulator often copy the entire accumulator for each iteration, like this:

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 的长度。

¥This code is ill-performing, because each iteration has to copy the entire allNames object, which could be big, depending how many unique names there are. This code has worst-case O(N^2) performance, where N is the length of names.

更好的选择是在每次迭代时改变 allNames 对象。但是,如果 allNames 无论如何都会发生变化,你可能需要将 reduce() 转换为简单的 for 循环,这样会更清晰:

¥A better alternative is to mutate the allNames object on each iteration. However, if allNames gets mutated anyway, you may want to convert the reduce() to a simple for loop instead, which is much clearer:

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;
}

因此,如果你的累加器是一个数组或一个对象,并且你在每次迭代时复制该数组或对象,则可能会意外地在代码中引入二次复杂度,导致在处理大数据时性能迅速下降。这在现实世界的代码中已经发生过 - 例如参见 通过 1 行更改使 Tanstack Table 速度提高 1000 倍

¥Therefore, if your accumulator is an array or an object and you are copying the array or object on each iteration, you may accidentally introduce quadratic complexity into your code, causing performance to quickly degrade on large data. This has happened in real-world code — see for example Making Tanstack Table 1000x faster with a 1 line change.

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

¥Some of the acceptable use cases of reduce() are given above (most notably, summing an array, promise sequencing, and function piping). There are other cases where better alternatives than reduce() exist.

  • 展平数组的数组。请改用 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() 是最佳选择,文档和语义变量命名可以帮助减轻可读性缺陷。

¥In cases where reduce() is the best choice, documentation and semantic variable naming can help mitigate readability drawbacks.

规范

Specification
ECMAScript Language Specification
# sec-array.prototype.reduce

¥Specifications

浏览器兼容性

BCD tables only load in the browser

¥Browser compatibility

也可以看看