# reduce

返回:array 操作从入门到精通的实用

按函数中指定的值累加。
arr.reduce()方法会对传入的数组元素进行组合,如例子中的 x 和 y,在每个方法中的返回值会注入到 x 中,然后继续与下一个 y 值进行操作,所有操作完成后返回最终结果,这个方法有第二个参数用来设置默认值到 x,但默认值不是必填项。
为了方便理解,我把代码中 sum 当做例子来进行拆解

const arr = [1, 2, 3, 4, 5, 6];
const reduced = arr.reduce((total, current) => total + current);
console.log(reduced);
// 21
1
2
3
4

# 语法

返回:reduce

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
1

# 参数

返回:reduce

  • callback: 执行数组中每个值 (如果没有提供 initialValue则第一个值除外)的函数,包含四个参数:
  • accumulator: 累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或 initialValue(见于下方)。
  • currentValue: 数组中正在处理的元素。
  • index(可选): 数组中正在处理的当前元素的索引。 如果提供了 initialValue,则起始索引号为 0,否则从索引 1 起始。
  • array(可选): 调用 reduce()的数组
  • initialValue(可选): 作为第一次调用 callback 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素在没有初始值的空数组上调用 reduce 将报错

# 基础使用

返回:reduce

单看概念有点绕,reduce 到底有什么用呢?只要记住 reduce 方法的核心作用就是聚合即可
何谓聚合?操作一个已知数组来获取一个任意类型的值就叫做聚合,这种情况下用 reduce 准没错,下面来看几个实际应用:

  • 聚合为数字:数组元素求和、求积、求平均数等
// 求总分
const sum = (arr) => arr.reduce((total, { score }) => total + score, 0);
// 求平均分
const average = (arr) =>
  arr.reduce((total, { score }, i, array) => {
    // 第n项之前均求和、第n项求和后除以数组长度得出平均分
    const isLastElement = i === array.length - 1;
    return isLastElement ? (total + score) / array.length : total + score;
  }, 0);
const arr = [
  { name: "Logan", score: 89 },
  { name: "Emma", score: 93 },
];
expect(sum(arr)).toBe(182);
expect(average(arr)).toBe(91);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 用伪代码解析求总分执行顺序如下:
const arr = [
  { name: "Logan", score: 89 },
  { name: "Emma", score: 93 },
];
const initialValue = 0;
let total = initialValue;
for (let i = 0; i < arr.length; i++) {
  const { score } = arr[i];
  total += score;
}
expect(total).toBe(182);
1
2
3
4
5
6
7
8
9
10
11

通过上方例子大家应该基本了解了 reduce 的执行机制,下面就来看下其他实际应用场景。

# 聚合为字符串

const getIntro = (arr) =>
  arr.reduce(
    (str, { name, score }) => `${str}${name}'s score is ${score};`,
    ""
  );

const arr = [
  { name: "Logan", score: 89 },
  { name: "Emma", score: 93 },
];
expect(getIntro(arr)).toBe("Logan's score is 89;Emma's score is 93;");
1
2
3
4
5
6
7
8
9
10
11

# 聚合为对象

下方代码生成一个 key 为分数,value 为对应分数的姓名数组的对象。

const scoreToNameList = (arr) =>
  arr.reduce((map, { name, score }) => {
    (map[score] || (map[score] = [])).push(name);
    return map;
  }, {});

const arr = [
  { name: "Logan", score: 89 },
  { name: "Emma", score: 93 },
  { name: "Jason", score: 89 },
];
expect(scoreToNameList(arr)).toEqual({
  89: ["Logan", "Jason"],
  93: ["Emma"],
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 深入理解

返回:reduce

# 何为未传入初始值

大部分实现 reduce 的文章中,通过检测第二个参数是否为 undefined 来判定是否传入初始值,这是错误的。 未传入就是严格的未传入,只要传入了值,哪怕是 undefined 或者 null,也会将其作为初始值。

const arr = [1];

// 未传入初始值,将数组第一项作为初始值
expect(arr.reduce((initialVal) => initialVal)).toBe(1);
// 传入 undefined 作为初始值,将 undefined 作为初始值
expect(arr.reduce((initialVal) => initialVal, undefined)).toBeUndefined();
// 传入 null 作为初始值,将 null 作为初始值
expect(arr.reduce((initialVal) => initialVal, null)).toBeNull();
1
2
3
4
5
6
7
8

所以自己实现 reduce 时可以通过 arguments.length 判断是否传入了第二个参数,来正确判定是否传入了初始值。

# 未传入初始值时的初始值

大部分实现 reduce 的文章中,reduce 方法未传入初始值时,直接使用数组的第一项作为初始值,这也是错误的。
未传入初始值时应使用数组的第一个不为空的项作为初始值。
不为空是什么意思呢,就是并未显式赋值过的数组项,该项不包含任何实际的元素,不是 undefined,也不是 null,在控制台中打印表现为 empty,我目前想到的有三种情况:

new Array(n),数组内均为空项;
字面量数组中逗号间不赋值产生空项;
显式赋值数组 length 属性至数组长度增加,增加项均为空项。

测试代码如下:

const arr1 = new Array(3);
console.log(arr1); // [empty × 3]
arr1[2] = 0;
console.log(arr1); // [empty × 2, 0]
// 第一、第二项为空项,取第三项作为初始值
expect(arr1.reduce((initialVal) => initialVal)).toBe(0);

const arr2 = [, , true];
console.log(arr2); // [empty × 2, true]
// 第一、第二项为空项,取第三项作为初始值
expect(arr2.reduce((initialVal) => initialVal)).toBe(true);

const arr3 = [];
arr3.length = 3; // 修改length属性,产生空
console.log(arr3); // [empty × 3]
arr3[2] = "string";
console.log(arr3); // [empty × 2, "string"]
// 第一、第二项为空项,取第三项作为初始值
expect(arr3.reduce((initialVal) => initialVal)).toBe("string");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 空项跳过迭代

大部分实现 reduce 的文章中,都是直接用 for 循环一把梭,将数组每项代入 callback 中执行,这还是错的。

  • 错误一:
    上面说了未传入初始值时使用数组的第一个非空项作为初始值,这种情况下,第一个非空项及其之前的空项均不参与迭代。
const arr = new Array(4);
arr[2] = "Logan";
arr[3] = "Emma";

let count = 0; // 记录传入reduce的回调的执行次数
const initialVal = arr.reduce((initialValue, cur) => {
  count++;
  return initialValue;
});
// 未传入初始值,跳过数组空项,取数组第一个非空项作为初始值
expect(initialVal).toBe("Logan");
// 被跳过的空项及第一个非空项均不参与迭代,只有第四项'Emma'进行迭代,故count为1
expect(count).toBe(1);
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 错误二:
    迭代过程中,空项也会跳过迭代。
const arr = [1, 2, 3];
arr.length = 10;
console.log(arr); // [1, 2, 3, empty × 7]

let count = 0; // 记录传入reduce的回调的执行次数
arr.reduce((acc, cur) => {
  count++;
  return acc + cur;
}, 0);

// arr中第三项之后项均为空项,跳过迭代,故count为3
expect(count).toBe(3);
1
2
3
4
5
6
7
8
9
10
11
12

自己实现 reduce 时,可以通过i in array判断数组第 i 项是否为空项。

# 实现 reduce

返回:reduce

说完了一些坑点,下面就来实现一个 reduce:

Array.prototype._reduce = function(callback) {
  // 省略参数校验,如this是否是数组等
  const len = this.length;
  let i = 0;
  let accumulator;

  // 传入初始值则使用
  if (arguments.length >= 2) {
    accumulator = arguments[1];
  } else {
    // 未传入初始值则从数组中获取
    // 寻找数组中第一个非空项
    while (i < len && !(i in this)) {
      i++;
    }

    // 未传入初始值,且数组无非空项,报错
    if (i >= len) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
    // 此处 i++ ,先返回i,即将数组第一个非空项作为初始值
    // 再+1,即数组第一个非空项跳过迭代
    accumulator = this[i++];
  }

  while (i < len) {
    // 数组中空项不参与迭代
    if (i in this) {
      accumulator = callback(accumulator, this[i], i, this);
    }

    i++;
  }
  return accumulator;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# reduce 拓展用法

返回:reduce

# 扁平化数组

Array.prototype._flat = function(depth = 1) {
  const flatBase = (arr, curDepth = 1) => {
    return arr.reduce((acc, cur) => {
      // 当前项为数组,且当前扁平化深度小于指定扁平化深度时,递归扁平化
      if (Array.isArray(cur) && curDepth < depth) {
        return acc.concat(flatBase(cur, ++curDepth));
      }
      return acc.concat(cur);
    }, []);
  };
  return flatBase(this);
};
1
2
3
4
5
6
7
8
9
10
11
12

# 合并多次数组迭代操作

相信大家平时会遇到多次迭代操作一个数组,比如将一个数组内的值求平方,然后筛选出大于 10 的值,可以这样写:

function test(arr) {
  return arr.map((x) => x ** 2).filter((x) => x > 10);
}
1
2
3

只要是多个数组迭代操作的情况,都可以使用 reduce 代替:

function test(arr) {
  return arr.reduce((arr, cur) => {
    const square = cur ** 2;
    return square > 10 ? [...arr, square] : arr;
  }, []);
}
1
2
3
4
5
6

# 串行执行 Promises

function runPromisesSerially(tasks) {
  return tasks.reduce((p, cur) => p.then(cur), Promise.resolve());
}
1
2
3

# 执行函数组合

function compose(...fns) {
  // 初始值args => args,兼容未传入参数的情况
  return fns.reduce(
    (a, b) => (...args) => a(b(...args)),
    (args) => args
  );
}
1
2
3
4
5
6
7

# 万物皆可 reduce

返回:reduce

reduce 方法是真的越用越香,其他类型的值也可转为数组后使用 reduce:

  • 字符串:[...str].reduce()
  • 数字:Array.from({ length: num }).reduce()
  • 对象:
Object.keys(obj).reduce();
Object.values(obj).reduce();
Object.entries(obj).reduce();
1
2
3

# 按属性对 object 分类

var people = [
  { name: "Alice", age: 21 },
  { name: "Max", age: 20 },
  { name: "Jane", age: 20 },
];

function groupBy(objectArray, property) {
  return objectArray.reduce(function(acc, obj) {
    var key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}

var groupedPeople = groupBy(people, "age");
// groupedPeople is:
// {
//   20: [
//     { name: 'Max', age: 20 },
//     { name: 'Jane', age: 20 }
//   ],
//   21: [{ name: 'Alice', age: 21 }]
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26