先记住这个答案
reduce 从最小的下标向最大的下标处理,reduceRight 正好相反,从最后一项往前处理。但两者回调的参数顺序是一样的,第一个参数都是累加器,第二个才是当前元素。有初始值时,初始值就是第一步的累加器;没有初始值时,reduce 用第一个元素当起点,reduceRight 用最后一个元素当起点。对于加法、乘法这类交换顺序不影响结果的运算,两者结果相同;字符串拼接、减法、函数组合这类和顺序有关的运算,结果就不同了。拿不准时,把前两三步手动展开写出来,一看就知道。
reduceRight只是遍历方向相反,参数顺序不变- 可交换的运算两者结果相同,减法、拼接会不同
compose用reduceRight,而pipe用reducereverse().reduce()会改掉原数组,reduceRight不会
顺序敏感的运算结果不同
用拼接和减法把两种方向的每一步展开看。
const arr = ['a', 'b', 'c']
console.log(arr.reduce((acc, x) => acc + x)) // ('a' + 'b') + 'c'
console.log(arr.reduceRight((acc, x) => acc + x)) // ('c' + 'b') + 'a'
console.log([10, 3, 2].reduce((acc, x) => acc - x)) // (10 - 3) - 2
console.log([10, 3, 2].reduceRight((acc, x) => acc - x)) // (2 - 3) - 10查看输出与解释
abc
cba
5
-11减法的例子最能说明问题。reduce 没有初始值,拿 10 当累加器,先减 3 再减 2,得到 5。reduceRight 拿最后一项 2 当累加器,先减 3 得到 -1,再减 10 得到 -11。两个结果相差很大,原因就是减法不满足交换律。
加法没有这个问题,[1, 2, 3] 不管从哪边加都是 6。所以求和、计数、取最大值用哪个都行,习惯上用 reduce。
最典型的用途:函数组合
函数组合 compose(f, g)(x) 的含义是 f(g(x)),也就是先执行最右边的函数,结果再交给左边的。这个「从右往左」正好对应 reduceRight。
compose 和 pipeJavaScriptconst compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x)
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x)
const add1 = (x) => x + 1
const double = (x) => x * 2
console.log(compose(add1, double)(5)) // add1(double(5))
console.log(pipe(add1, double)(5)) // double(add1(5))查看输出与解释
11
12compose 的写法和数学上的函数组合一致,Redux 的 compose 就是这个语义,中间件从右往左包装。pipe 按书写顺序从左往右执行,读起来更像流水线,很多函数式工具库两个都提供。面试手写 compose 时,用 reduceRight 是最直接的写法。
- 为什么不写成
reverse().reduce() - 结果看起来一样,但有两个差别。
reverse会原地修改数组,fns.reverse()之后传进来的函数数组顺序就被改掉了,下次调用会出错。另外回调第三个参数的下标也不同:reduceRight给的是元素在原数组里的下标,reverse之后给的是新顺序里的下标。
容易答错的地方
- 以为
reduceRight的回调参数也反过来了 - 参数顺序永远是
(累加器, 当前值, 下标, 数组),而reduceRight只改变遍历方向。把回调写成(x, acc) => ...会把两者弄反,减法、拼接的结果就错了。 - 以为
reduceRight就是函数式语言里的右折叠 - Haskell 的
foldr对[10, 3, 2]做减法,计算的是10 - (3 - 2),结果是 9;JavaScript 的reduceRight计算的是(2 - 3) - 10,结果是 -11。原因是reduceRight的累加器还是放在左边。说「reduceRight等于右折叠」要加上这个前提,面试官追问时才不会被问住。 - 空数组不传初始值
[].reduceRight((a, b) => a + b)会抛TypeError: Reduce of empty array with no initial value。只有一个元素且没有初始值时,回调不会被调用,直接返回那个元素。
面试官还会怎么问?
稀疏数组上 reduceRight 怎么处理空位?
和 reduce 一样跳过空位,回调不会在空位上被调用。没有初始值时,它会从末尾往前找到第一个真实存在的元素作为起点,而不是直接拿 length - 1 上的值。
手写一个 reduceRight?
从 length - 1 循环到 0,用 i in arr 跳过空位。没有初始值时,先往前找到第一个存在的元素作为累加器,找不到就抛 TypeError;然后对剩下的每一项调用 callback(acc, arr[i], i, arr),最后返回累加器。
函数组合里如果有异步函数,怎么写?
让累加器始终是 Promise:fns.reduceRight((p, fn) => p.then(fn), Promise.resolve(x))。每个函数等上一个的结果兑现后才执行,顺序仍然是从右往左。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。