Array.prototype.reduce

Array.prototype.reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。对空数组时不会执行回调函数。

语法

语法:

arr.reduce(callbackfn [, initialValue]);

类型声明

interface Array<T> {
  reduce(
    callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T
  ): T;

  reduce(
    callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T,
    initialValue: T
  ): T;

  reduce<U>(
    callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U,
    initialValue: U
  ): U;
}

参数说明:

参数 说明 类型
callbackfn 回调函数,用于遍历数组成员时执行 function
initialValue (可选)累加器初始值,用作第一个调用回调函数的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用将报错。 any

callbackfn 函数的参数:

  • previousValue:累加器累加回调的返回值,它是上一次调用回调时返回的累积值,或 initialValue
  • currentValue:当前数组中处理的元素
  • index:数组中正处理的当前元素的索引
  • array:被调用的数组

返回值:

返回函数累计处理的结果。

方法说明

reduce() 方法为数组中的每一个元素依次执行 callback 回调函数,不包括数组中被删除或从未被赋值的元素。

回调函数第一次执行时,acccurrentValue 的取值有两种情况:

  • 回调函数参数取值问题
    • 提供 initialValue,累加器 acc 取值为 initialValuecurrentValue 取数组中的第一个值
    • 没有提供 initialValue,累加器 acc <strong style="color:red">取数组中的第一个值作为初始值</strong>,currentValue 取数组中的第二个值。
  • 回调函数调用问题
    • 如果提供 initialValue,从索引 0 开始执行回调函数。
    • 如果没有提供 initialValuereduce 会从索引 1 的地方开始执行回调函数,跳过第一个索引。
    • 如果数组为空且没有提供 initialValue,会抛出 TypeError
    • 如果数组仅有一个元素(无论位置如何)并且没有提供 initialValue, 或者有提供 initialValue 但是数组为空,那么此唯一值将被返回并且 callback 不会被执行。

假如运行下段代码:

[0, 1, 2, 3, 4].reduce((acc, val, index, arr) => acc + val);

回调函数被调用四次,每次调用的参数和返回值如下表所示。

callback 回调函数 acc 累加器 val 当前值 index 当前索引 arr 返回值
first call 0 1 1 [0, 1, 2, 3, 4] 1
second call 1 2 2 [0, 1, 2, 3, 4] 3
third call 3 3 3 [0, 1, 2, 3, 4] 6
fourth call 6 4 4 [0, 1, 2, 3, 4] 10

reduce() 方法最终的返回值为 10。

如果你打算提供一个初始值作为 reduce 方法的第二个参数,以下是运行过程及结果。

[0, 1, 2, 3, 4].reduce((acc, val, index, arr) => accumulator + currentValue, 10);
callback 回调函数 acc 累加器 val 当前值 index 当前索引 arr 返回值
first call 10 0 0 [0, 1, 2, 3, 4] 10
second call 10 1 1 [0, 1, 2, 3, 4] 11
third call 11 2 2 [0, 1, 2, 3, 4] 13
fourth call 13 3 3 [0, 1, 2, 3, 4] 16
fifth call 16 4 4 [0, 1, 2, 3, 4] 20

reduce() 方法最终的返回值为 20。

代码示例

  • 将数组转为对象
  • 展开更大的数组
  • 在一次遍历中进行两次计算
  • 将映射和过滤函数组合
  • 按顺序运行异步函数

聚合为数字

数组成员为数字类型时。

const res = [1, 2, 3, 4, 5].reduce((acc, item) => acc + item, 0);

console.log(res);
// 15

数组成员为对象类型时。

const arr = [{ total: 1 }, { total: 2 }, { total: 3 }, { total: 4 }, { total: 5 }];

const res = arr.reduce((acc, { total }) => acc + total, 0);

console.log(res);
// 15

聚合为字符串

将数组的每项转换为固定格式的字符串,每项直接以分号作为分隔。

const arr = [
  { key: 'foo', value: 1 },
  { key: 'bar', value: 2 },
  { key: 'baz', value: 3 },
];

const res = arr.reduce((acc, { key, value }) => acc + `${key}=${value}&`, '?');

console.log(res);
// "?foo=1&bar=2&baz=3&"

聚合为对象

只要目标是将数组聚合为唯一的元素时,都可以考虑使用 reduce

const arr = [
  { id: 1, type: 'a', name: 'foo' },
  { id: 2, type: 'b', name: 'bar' },
  { id: 3, type: 'c', name: 'baz' },
];

const res = arr.reduce((acc, { id, type, name }) => {
  acc[id] = { type, name };
  return acc;
}, {});

console.log(res);
// { 1: { name: 'foo', type: 'a'}, 2: { name: 'bar', type: 'b'}, { name: 'baz', type: 'c' }}

初始值的必要性

提供初始值通常更安全。

没有提供初始值。

const maxCallback = ( pre, current ) => Math.max( pre.x, current.x )

[{ x: 22}, { x: 42}].reduce(maxCallback)
// 42

[{ x: 22}].reduce(maxCallback)
// { x: 22 }

[].reduce(maxCallback)
// TypeError

提供初始值。

const maxCallback = ( max, current ) => Math,max( max, current )

[{ x: 22 }, { x: 42 }].map( el => el.x ).reduce( maxCallback2, -Infinity );

数组求和、求积和最大值

// 数组求和
const sum = [0, 1, 2, 3].reduce((acc, cur) => acc + cur, 0);
// 6

// 数组求积
const product = [1, 2, 3, 4, 5].reduce((a, b) => a * b, 1);
// 120

// 数组最大值
const max = [1, 2, 3, 4, 5].reduce((a, b) => (a > b ? a : b));
// 5

数组元素

找出长度最长的数组元素。

const findLongest = (entries) =>
  entries.reduce((prev, cur) => (cur.length > prev.length ? cur : prev), '');

console.log(findLongest([1, 2, 3, 'ab', 4, 'bcd', 5, 6785, 4]));
// 'bcd'

二维数组扁平化

const arr = [
  [0, 1],
  [2, 3],
  [4, 5],
];

const res = arr.reduce((a, b) => a.concat(b), []);

console.log(res);
// [0, 1, 2, 3, 4, 5]

你也可以写成箭头函数的形式:

var flattened = [
  [0, 1],
  [2, 3],
  [4, 5],
].reduce((acc, cur) => acc.concat(cur), []);

计算数组成员次数

const names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

const countedNames = names.reduce((allNames, name) => {
  if (name in allNames) {
    allNames[name]++;
  } else {
    allNames[name] = 1;
  }
  return allNames;
}, {});

// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }

单次遍历多次计算

const arr = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];

function reduceMaxMin(acc, value) {
  reuturn {
    min: Math.min(acc.min, value),
    max: Math.max(acc.max, value)
  }
}

const initMinMax = {
  min: Number.MIN_VALUE,
  max: Number.MAX_VALUE
}

const minMax = arr.reduce(reduceMaxMin, initMinMax);

console.log(minMax);
// { min: 0.2, max: 5.5}

兼容性代码

if (!Array.prototype.reduce) {
  Object.defineProperty(Array.prototype, 'reduce', {
    value: function (callback) {
      if (this === null) {
        throw new TypeError('Array.prototype.reduce called on null or undefined');
      }

      if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function');
      }

      // 将数组对象化
      const obj = Object(this);

      const len = obj.length >>> 0;

      let index = 0;
      let accumulator;

      // 处理累加器(也就是 reduce 方法第二个参数)
      if (arguments.length >= 2) {
        // 累加器
        accumulator = arguments[1];
      } else {
        while (index < len && !(index in obj)) {
          index++;
        }

        if (index >= len) {
          throw new TypeError('Reduce of empty array with no initial value');
        }

        accumulator = obj[index++];
      }

      // 走有累加器的那种实现
      while (index < len) {
        if (index in obj) {
          accumulator = callback(accumulator, obj[index], index, obj);
        }

        index++;
      }

      return accumulator;
    },
  });
}

参考资料