Array.reduce()的一知半解

471 阅读5分钟

前言

Array.reduce() 知道这个方法很久了,但却很少使用它。

但是它非常强大,可以做很多事情,今天带大家掌握并使用它。

简介

reduce()  方法对数组中的每个元素按序 执行一个提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果 作为参数传入,最后将其结果汇总为单个返回值。

第一次执行回调函数时,不存在“上一次的计算结果”。如果需要回调函数从数组索引为 0 的元素开始执行,则需要传递初始值。否则,数组索引为 0 的元素将被用作初始值,迭代器将从第二个元素开始执行(即从索引为 1 而不是 0 的位置开始)。

语法

reduce(callbackFn)
reduce(callbackFn, initialValue)

我们主要来看看 callbackFn 的参数:

  • accumulator:上一次调用 callbackFn 的结果。在第一次调用时,如果指定了 initialValue 则为指定的值,否则为 array[0] 的值。
  • currentValue
  • currentIndex
  • array: 调用了 reduce() 的数组本身。

它最后会将 accumulator 作为返回值返回。

了解这些基本信息之后,我们来看看它可以做些什么?

使用

求和

值数组

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

let result = arr.reduce((acc, cur) => acc + cur)

console.log(result)

对象数组

const arr = [{ x: 1 }, { x: 2 }, { x: 3 }]

let result = arr.reduce((acc, cur) => acc + cur.x, 0)

console.log(result)

对象数组我们使用了初始值,是因为 arr[0] 无法作为 reduce 的初始值,我们必须得设置初始值。

有个小疑问,上面两个求和的 reduce 回调函数各执行了几次?

展平嵌套数组

用 reduce 来实现 Array.flat()

示例1:

const flattened = [
  [0, 1],
  [2, 3],
  [4, 5],
].reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
// flattened 的值是 [0, 1, 2, 3, 4, 5]

示例2:

var arr = [0, 1, 2, [3, [4, 5], 6], 7, 8, 9];

function flat(arr) {
  return arr.reduce((acc, current) => {
    if(current instanceof Array) {
      return [...acc, ...flat(current)]
    } else {
      return [...acc, current]
    }
  }, [])
}

console.log(flat(arr))  // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

示例3:

var arr = [0, 1, 2, [3, [4, 5], 6], 7, 8, 9];

function flat(arr, acc) {
  acc = acc || []
  for(let i = 0; i < arr.length; i++) {
    if(arr[i] instanceof Array) {
      acc = [...acc, ...flat(arr[i])]
    } else {
      acc = [...acc, arr[i]]
    }
  }
  return acc
}

console.log(flat(arr)) // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

统计对象总值的出现次数

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

const countedNames = names.reduce((allNames, name) => {
  const currCount = allNames[name] ?? 0;
  return {
    ...allNames,
    [name]: currCount + 1,
  };
}, {});
// countedNames 的值是:
// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }

初始值为 {} ,我们可以给它添加属性

通过上面三个示例,我们知道 reduce 可以返回任何类型的值,如 number,array,object。这跟我们设置的初始值息息相关。

按属性对 对象进行分组

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

function groupBy(arr, prop) {
    return arr.reduce((arr, cur) => {
        const key = cur[prop]
        const curGroup = arr[key] ?? []
        return {
            ...arr,
            [key]: [...curGroup, cur]
        }
    }, {})
}

const groupedPeople = groupBy(people, "age");
console.log(groupedPeople);

// {
//     '20': [ { name: 'Max', age: 20 }, { name: 'Jane', age: 20 } ],
//     '21': [ { name: 'Alice', age: 21 } ]
// }

如果想要返回数组类型,可将初始值设为 []

按属性整合数组中的值

const friends = [
  {
    name: "Anna",
    books: ["Bible", "Harry Potter"],
    age: 21,
  },
  {
    name: "Bob",
    books: ["War and peace", "Romeo and Juliet"],
    age: 26,
  },
  {
    name: "Alice",
    books: ["The Lord of the Rings", "The Shining"],
    age: 18,
  },
];

const getAllBy = (arr, prop, initialValue) => {
  return arr.reduce(
    (accumulator, currentValue) =>
      currentValue[prop] instanceof Array
        ? [...accumulator, ...currentValue[prop]]
        : [...accumulator, currentValue[prop]],
        initialValue ?? []
  );
};

const allbooks = getAllBy(friends, "books", ["Alphabet"]);
// [
//     'Alphabet',
//     'Bible',
//     'Harry Potter',
//     'War and peace',
//     'Romeo and Juliet',
//     'The Lord of the Rings',
//     'The Shining'
// ]
const allnames = getAllBy(friends, "name");
// [ 'Anna', 'Bob', 'Alice' ]

去重

const myArray = ["a", "b", "a", "b", "c", "e", "e", "c", "d", "d", "d", "d"];
const myArrayWithNoDuplicates = myArray.reduce((accumulator, currentValue) => {
  if (!accumulator.includes(currentValue)) {
    return [...accumulator, currentValue];
  }
  return accumulator;
}, []);

代替 .filter().map()

使用 filter() 和 map() 会遍历数组两次,但是你可以使用 reduce() 只遍历一次并实现相同的效果,从而更高效。

const numbers = [-5, 6, 2, 0];

const doubledPositiveNumbers = numbers.reduce((accumulator, currentValue) => {
  if (currentValue > 0) {
    const doubled = currentValue * 2;
    return [...accumulator, doubled];
  }
  return accumulator;
}, []);

console.log(doubledPositiveNumbers); // [12, 4]

按顺序执行promise

function runPromiseInSequence(arr, input) {
  return arr.reduce(
    (promiseChain, currentFunction) => promiseChain.then(currentFunction),
    Promise.resolve(input),
  );
}

// Promise 函数 1
function p1(a) {
  return new Promise((resolve, reject) => {
    resolve(a * 5);
  });
}

// Promise 函数 2
function p2(a) {
  return new Promise((resolve, reject) => {
    resolve(a * 2);
  });
}

// 函数 3——将由 `.then()` 包装在已解决的 Promise 中
function f3(a) {
  return a * 3;
}

// Promise 函数 4
function p4(a) {
  return new Promise((resolve, reject) => {
    resolve(a * 4);
  });
}

const promiseArr = [p1, p2, f3, p4];
runPromiseInSequence(promiseArr, 10).then(console.log); // 1200

有点像:

new Promise((resolve) => {
  resolve(1);
})
  .then((res) => new Promise((resolve) => resolve(res + 2)))
  .then((res) => new Promise((resolve) => resolve(res + 3)))
  .then(res => console.log(res)) // 6

使用 reduce 可灵活安排promise的执行顺序。

使用函数组合实现管道

// 组合使用的构建块
const double = (x) => 2 * x;
const triple = (x) => 3 * x;
const quadruple = (x) => 4 * x;

// 函数组合,实现管道功能
const pipe =
  (...functions) =>
  (initialValue) =>
    functions.reduce((acc, fn) => fn(acc), initialValue);

// 组合的函数,实现特定值的乘法
const multiply6 = pipe(double, triple);
const multiply9 = pipe(triple, triple);
const multiply16 = pipe(quadruple, quadruple);
const multiply24 = pipe(double, triple, quadruple);

// 用例
multiply6(6); // 36
multiply9(9); // 81
multiply16(16); // 256
multiply24(10); // 240

这个有点意思,像递归一样,却每次执行不同的函数。其实 reduce 本身就是一个强大的递归函数。管道的概念没使用过,今后看看能不能用它解决一些问题。

在稀疏数组中使用 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()

const arrayLike = {
  length: 3,
  0: 2,
  1: 3,
  2: 4,
};
console.log(Array.prototype.reduce.call(arrayLike, (x, y) => x + y));
// 9

遍历类数组对象时,遍历次数取决于 length,从下标 0 到 length - 1,其他下标不会读取。

const arrayLike = {
  length: 3,
  0: 2,
  1: 3,
  3: 4,
  A: "test",
};
console.log(Array.prototype.reduce.call(arrayLike, (x, y) => x + y)); 
// 5

ployfill

Array.prototype.reduceSelf = function(cb, initailVal) {
    let arr = this
    let len = arr.length
    let acc = initailVal
    
    for (let i = 0; i < len; i++) {
        if(acc === undefined) {
            acc = arr[0]
            continue
        } else {
            acc = cb(acc, arr[i], i)
        }
    }

    return acc
}

有什么问题,可以留言告诉我。

总结

你还可以用 reduce 实现 斐波那契数列,它应用场景还是很多的。但值得注意的是:

  • 使用参数 accumulator 时,尽量使用 展开语法(...)或 其他复制方法来创建新的数组 或 对象 作为累加器。
  • reduce 没有初始值时,索引 0 作为初始值,从索引 1 开始遍历。
  • reduce 是通过递归实现的,有时间写个 pollfill。

动手点个赞吧!