先记住这个答案
reduce 不传初始值时,会把数组的第一个元素当作累加器的起点,然后从第二个元素开始调用回调。空数组没有第一个元素,没法开始,所以抛出 TypeError: Reduce of empty array with no initial value。传了初始值之后,空数组会直接返回这个初始值,回调一次都不执行。初始值要符合业务含义:求和用 0,拼数组用 [],但求最大值这类场景不能填 0,要单独处理空数组。
- 不传初始值时,第一个元素就是累加器起点
- 空数组加无初始值,直接抛
TypeError - 传了初始值,空数组直接返回初始值
- 求最大值填 0 会让全负数的结果出错
最常见的触发场景:filter 之后再 reduce
接口返回的数组明明有数据,页面却偶尔报错,通常是因为中间有一步 filter。某些筛选条件下一项都没剩,后面的 reduce 拿到的就是空数组。
const selected = [-2, 0].filter(value => value > 0) // 没有正数,得到 []
try {
selected.reduce((sum, value) => sum + value) // 不传初始值
} catch (error) {
console.log(error.name)
}
let calls = 0
const total = selected.reduce((sum, value) => { calls++; return sum + value }, 0)
console.log(total, calls) // 直接返回初始值 0,回调没执行
console.log([9].reduce((acc, value) => acc + value)) // 单元素:直接返回 9查看输出与解释
TypeError
0 0
9不传初始值时抛了 TypeError。传了 0 之后,空数组直接返回 0,calls 也是 0,说明这个 0 不是算出来的,是初始值原样返回。最后一行是单元素数组不传初始值的情况:元素 9 被拿来当累加器,没有第二个元素要合并,回调一次都没调用,直接返回 9。
这也提醒我们:想在回调里做校验或格式转换,不传初始值时第一个元素永远不会经过回调。
初始值不能一律填 0
补一个 0 能消掉报错,但不一定对。初始值应该是「一个元素都没有时,这个结果该是什么」。
- 按运算选初始值
- 求和用 0,求积用 1,拼数组用
[],拼对象用{},这些都是加进去不改变结果的值。求最大值就不一样了:数组是[-5, -3]时,初始值填 0 会得到 0,而数组里根本没有 0。 求最大值的正确做法是先判断空数组:arr.length ? Math.max(...arr) : null,或者用-Infinity当初始值。如果结果要展示或者转 JSON,-Infinity转出来是null,还不如直接返回null让调用方明确处理「没有数据」这种情况。 用对象或数组做初始值时,要在每次调用时新建,不要把一个共享的对象传进去,否则多次聚合会互相污染。
容易答错的地方
- 用
try...catch捕获错误然后返回 0 - 这会把回调里真正的程序错误也一起吞掉。空数组是可以预见的情况,应该用初始值或者提前判断,而不是靠捕获异常兜底。
- 以为
length大于 0 就一定不会报错 - 稀疏数组
new Array(3)的length是 3,但里面一个元素都没有,reduce会跳过空位,照样找不到起点,同样抛TypeError。
面试官还会怎么问?
显式传 undefined 当初始值算传了吗?
算。reduce 判断的是有没有传第二个参数,不是它的值。[].reduce(fn, undefined) 返回 undefined,不报错。但非空数组的第一次回调里累加器就是 undefined,求和会得到 NaN。
用 TypeScript 写 reduce 要注意什么?
不传初始值时,累加器类型只能和元素类型一样。想把数组聚合成另一种类型,比如把字符串数组变成对象,就必须传初始值,并通过泛型写明类型:arr.reduce<Record<string, number>>((acc, key) => acc, {})。
reduceRight 的规则一样吗?
一样。只是方向反过来,从最后一个元素开始当累加器。空数组不传初始值同样抛 TypeError。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。