Array.prototype.reduce()
reduce()
方法是 Array
实例的方法,它按顺序对数组中的每个元素执行用户提供的“reducer”回调函数,并将前一个元素计算的返回值传递给它。在所有数组元素上运行 reducer 的最终结果是一个单一的值。
回调函数第一次运行时,没有“前一个计算的返回值”。如果提供了初始值,则可以使用它来代替。否则,数组中索引为 0 的元素将用作初始值,并且迭代将从下一个元素(索引 1 而不是索引 0)开始。
试一试
语法
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
再次传递给 callbackFn
。accumulator
的最终值(即在数组的最后一次迭代中从 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 的数组的行为将有所不同,如下例所示
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 |
索引 |
返回值 | |
---|---|---|---|---|
第一次调用 | 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()
的第二个参数传递。
[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
,以便每个项目都通过您的函数。
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 语言规范 # sec-array.prototype.reduce |
浏览器兼容性
BCD 表格仅在启用 JavaScript 的浏览器中加载。