Array.prototype.reduce()
reduce() 方法对数组中的每个元素按序执行一个用户提供的“reducer”回调函数,并将前一个元素计算的返回值作为输入。运行 reducer 遍历数组所有元素的最终结果是一个单一值。
第一次运行回调时,没有“前一个计算的返回值”。如果提供了一个初始值,则可以使用它来代替。否则,数组索引 0 处的元素用作初始值,迭代从下一个元素(索引 1 而不是索引 0)开始。
试一试
const array = [1, 2, 3, 4];
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array.reduce(
(accumulator, currentValue) => accumulator + currentValue,
initialValue,
);
console.log(sumWithInitial);
// Expected output: 10
语法
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() 方法是一个迭代方法。它按升序索引顺序遍历数组中的所有元素,并将其累积为一个单一值。每次 callbackFn 的返回值在下一次调用时再次作为 accumulator 传递给 callbackFn。accumulator 的最终值(即数组最后一次迭代中 callbackFn 返回的值)成为 reduce() 的返回值。有关这些方法如何工作的更多信息,请阅读迭代方法部分。
callbackFn 仅对具有已赋值的数组索引调用。对于稀疏数组中的空槽,它不会被调用。
与其他迭代方法不同,reduce() 不接受 thisArg 参数。callbackFn 总是以 undefined 作为 this 调用,如果 callbackFn 不是严格模式,则 undefined 会被替换为 globalThis。
reduce() 是函数式编程中的一个核心概念,在这种编程范式中,不能修改任何值,因此为了累积数组中的所有值,每次迭代都必须返回一个新的累积值。这个约定传播到 JavaScript 的 reduce():在可能的情况下,您应该使用展开语法或其他复制方法来创建新的数组和对象作为累加器,而不是修改现有的。如果您决定修改累加器而不是复制它,请记住仍要在回调中返回修改后的对象,否则下一次迭代将收到 undefined。但是,请注意复制累加器可能会导致内存使用量增加和性能下降——有关详细信息,请参阅何时不使用 reduce()。在这种情况下,为了避免糟糕的性能和难以阅读的代码,最好改用 for 循环。
reduce() 方法是通用的。它只期望 this 值具有 length 属性和以整数为键的属性。
边缘情况
如果数组只有一个元素(无论位置如何)并且没有提供 initialValue,或者如果提供了 initialValue 但数组为空,则将返回该单独的值,而不调用 callbackFn。
如果提供了 initialValue 并且数组不为空,则 reduce 方法将始终从索引 0 开始调用回调函数。
如果未提供 initialValue,则 reduce 方法对于长度大于 1、等于 1 和 0 的数组将表现不同,如以下示例所示:
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() 时会发生什么。
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 |
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)。
reduce() 在有初始值的情况下如何工作
在这里,我们使用相同的算法来减少相同的数组,但将 initialValue 设为 10,并作为第二个参数传递给 reduce():
[15, 16, 17, 18, 19].reduce(
(accumulator, currentValue) => accumulator + currentValue,
10,
);
回调函数将被调用五次,每次调用的参数和返回值如下:
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。
对象数组中值的和
要将对象数组中包含的值求和,您必须提供 initialValue,以便每个项目都通过您的函数。
const objects = [{ x: 1 }, { x: 2 }, { x: 3 }];
const sum = objects.reduce(
(accumulator, currentValue) => accumulator + currentValue.x,
0,
);
console.log(sum); // 6
函数顺序管道
pipe 函数接受一系列函数并返回一个新函数。当使用一个参数调用新函数时,这一系列函数会按顺序调用,每个函数接收前一个函数的返回值。
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 序列化本质上是上一节中演示的函数管道,只是以异步方式完成。
// 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 的相似性。
const asyncPipe =
(...functions) =>
(initialValue) =>
functions.reduce(async (acc, fn) => fn(await acc), initialValue);
将 reduce() 与稀疏数组一起使用
reduce() 会跳过稀疏数组中缺失的元素,但不会跳过 undefined 值。
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() 方法读取 this 的 length 属性,然后访问其键是非负整数且小于 length 的每个属性。
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 循环,只是我们现在每次迭代都返回新值,而不是在父作用域中修改变量。
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() 的原因是模仿函数式编程中不可变数据的实践。因此,坚持累加器不可变性的开发人员通常会为每次迭代复制整个累加器,如下所示:
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),其中 N 是 names 的长度。
一个更好的替代方法是在每次迭代中修改 allNames 对象。但是,如果 allNames 无论如何都会被修改,您可能希望将 reduce() 转换为 for 循环,这样会更清晰:
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));
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()。jsconst flattened = array.reduce((acc, cur) => acc.concat(cur), []);jsconst flattened = array.flat(); -
按属性对对象进行分组。改用
Object.groupBy()。jsconst groups = array.reduce((acc, obj) => { const key = obj.name; const curGroup = acc[key] ?? []; return { ...acc, [key]: [...curGroup, obj] }; }, {});jsconst groups = Object.groupBy(array, (obj) => obj.name); -
连接对象数组中包含的数组。改用
flatMap()。jsconst 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], []);jsconst allBooks = friends.flatMap((person) => person.books); -
删除数组中的重复项。改用
Set和Array.from()。jsconst uniqArray = array.reduce( (acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]), [], );jsconst 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]; }, []);jsconst 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()。这些方法还有一个额外的好处,即一旦结果确定,它们就会返回,而无需迭代整个数组。jsconst allEven = array.reduce((acc, cur) => acc && cur % 2 === 0, true);jsconst allEven = array.every((val) => val % 2 === 0);
在 reduce() 是最佳选择的情况下,文档和语义变量命名可以帮助减轻可读性方面的缺点。
规范
| 规范 |
|---|
| ECMAScript® 2026 语言规范 # sec-array.prototype.reduce |
浏览器兼容性
加载中…