数组的reduce方法的应用场景及相关面试题

1,786 阅读4分钟

reduce 最大的作用:从一个数组得到一个值,对数组中的每个元素执行 reducer 函数(升序执行)

reduce 打开相当于:

// 这里为了更加方便理解 统一加上initValue
[x1, x2, x3].reduce(f,initValue) 
// f起码是两个参数 
f(f(f(initValue,x1), x2), x3);

写reduce的核心其实很明了:

  • 找到initValue
  • 找到ff始终有两个参数 accitem

这样,能解决大部分的问题了,而且新手也有了线索,容易写reduce。

以下既是应用场景,也是很好练习的例子。

累加数组所有值

const sumFn = (acc, item) => acc + item;
const sum = [1, 2, 3, 4].reduce(sumFn, 0);
// 6
console.log(sum);

累加对象数组里的值

const sumFn = (acc, item) => acc + item.x;
const sum = [{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }].reduce(sumFn, 0);
// 6
console.log(sum);

降维数组,二维变成一维

// 之所以用concat不用push 是concat返回合并后的数组,而push返回数组长度
const flat = (acc, item) => acc.concat(item);
const res = [
  [0, 1],
  [2, 3],
  [4, 5]
].reduce(flat, []);
// [0, 1, 2, 3, 4, 5]
console.log(res);

扁平化任意维数组 --- 面试常考

const flat = (acc, item) => acc.concat(item);
const res = [ 0, 1, [2, 3, [4, 5]] ].reduce(flat, []);
// [0, 1, 2, 3, 4, 5]
console.log(res)
// 这里更新了,稍微抽象出一个函数,当前项是数组的话 flatten(item) 不是数组的话用concat合并下
const flatten = arr =>
  arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);

计算数组中每个元素出现的次数 --- 面试常考

const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const getCount = (obj, key) => {
  key in obj || (obj[key] = 0);
  obj[key]++;
  return obj;
};
const res = names.reduce(getCount, {});
// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }
console.log(res);

按属性对 object 分类 --- 面试常考

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

const getType = (obj, item) => {
  const key = item.age;
  key in obj || (obj[key] = []);
  obj[key].push(item);
  return obj;
};
const res = people.reduce(getType, {});
// { 20: [ { name: 'Max', age: 20 }, { name: 'Jane', age: 20 } ], 21: [{ name: 'Alice', age: 21 }] }
console.log(res);

这里可以稍微升级下,可以对任意字段进行分类

const classifyArray = (arr, keyClassified) => {
  const getType = (obj, item) => {
    const key = item[keyClassified];
    key in obj || (obj[key] = []);
    obj[key].push(item);
    return obj;
  };
  return arr.reduce(getType, {});
};

const people = [
  { id: 1, name: "Alice", age: 21, gender: 0 },
  { id: 2, name: "Max", age: 20, gender: 1 },
  { id: 3, name: "Jane", age: 20, gender: 0 }
];
// { '0': [ { id: 1, name: 'Alice', age: 21, gender: 0 }, { id: 3, name: 'Jane', age: 20, gender: 0 } ], '1': [ { id: 2, name: 'Max', age: 20, gender: 1 } ] }
console.log(classifyArray(people, "gender"));

使用扩展运算符处理对象数组中的数组

// 合并所有的书
var 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 sumArr = (oldArr, item) => {
  return [...oldArr, ...item.books];
};
const res = friends.reduce(sumArr, []);
// [ 'Bible', 'Harry Potter', 'War and peace', 'Romeo and Juliet', 'The Lord of the Rings', 'The Shining' ]
console.log(res);

数组去重 --- 面试常考

var myArray = ["a", "b", "a", "b", "c", "e", "e", "c", "d", "d", "d", "d"];
const addArr = (acc, item) => (acc.includes(item) ? acc : [...acc, item]);
const res = myArray.reduce(addArr, []);
// [ 'a', 'b', 'c', 'e', 'd' ]
console.log(res);

当然还有更简单粗暴的法子

const unique = arr => Array.from(new Set(arr));
// [ 'a', 'b', 'c', 'e', 'd' ]
console.log(
  unique(["a", "b", "a", "b", "c", "e", "e", "c", "d", "d", "d", "d"])
);

按顺序运行 promise --- 面试常考

// promise function 1
function p1(a) {
  return new Promise(resolve => {
    resolve(a * 5);
  });
}

// !!这个是普通函数哟
function f(a) {
  return a * 2;
}

// promise function 2
function p2(a) {
  return new Promise(resolve => {
    resolve(a - 2);
  });
}

const arr = [p1, f, p2];
const runOrderly = (acc, item) => acc.then(item);
const res = arr.reduce(runOrderly, Promise.resolve(10));
// 98
console.log(res);

再抽象下写个通用的顺序执行的函数

const runOrderly = (arr, initValue) =>
  arr.reduce((acc, item) => acc.then(item), Promise.resolve(initValue));

生成组合函数compose --- 面试常考

举个例子,就明白啥是组合函数了

const sum = (a, b) => a + b;
const len = str => str.length;
const addCurrency = str => "$" + str;

// 现在想要先 将两字符求和,然后求长度,再然后再长度前加个$,这就是一个新的函数,是已知函数的组合
const newFn = (a, b) => addCurrency(len(sum(a, b)));
console.log(newFn("xyz", "abc"));

但是不想像上面那样,地狱式嵌套的生成newFn,希望能 compose(addCurrency,len,sum),这里可以试试reduceRight

// compose(addCurrency,len,sum)
const compose = (...fns) => {
  return (...args) => {
    // 这里因为最后一个函数是两个参数,和别的不一样,所以这边单独把它扔出来,这样其他的都符合fn(acc)
    const lastFn = fns.pop();
    let initValue = lastFn(...args);
    const f = (acc, fn) => fn(acc);
    return fns.reduceRight(f, initValue);
  };
};
const newFn = compose(addCurrency, len, sum);
console.log(newFn("xyz", "abc"));

// 用箭头函数简化
const compose = (...fns) => (...args) =>
  fns.reduceRight((acc, fn) => fn(acc), fns.pop()(...args));

当然如果思维再厉害点,也可以用reduce

// compose(addCurrency,len,sum)
const compose = (...fns) =>
  fns.reduce((acc, cur) => (...args) => acc(cur(...args)));

这个法子很不容易想,但是假设现在只组合两个函数,len和sum

  • compose就相当于(...args) => len(sum(...args))
  • 而用reduce的话相当于(...args)=>[len,sum].reduce((acc,curFn)=>(...args) => acc(curFn(...args)))
  • [len,sum]换成别的数组也一样
  • 我这也是马后炮啦,下次我自己写估计又只会reduceRight

reduce 的坑

  • 空数组不能 reduce
  • reduce(f)没 initValue 的话,f 的第一次参数是arr[0] arr[1] 1 arr
  • reduce(f,initValue)有 initValue 的话,f 的第一次参数是initValue arr[0] 0 arr
  • f必须至少有两个参数,总共四个参数acc(累计器) cur(当前值) curIndex (当前索引) src (源数组)

可读性更高的话,可以传入 initValue,这样每次的操作都是一致的不容易出错。本文也是如此操作

引用