Array.reduce()还能这样用!!

833 阅读11分钟

大家都知道数组有一个reduce方法,它允许我们对数组中的每一个元素执行一个回调函数,从而将数组缩减为单个值。大家可能平常最常用的场景就是用它来进行数组求和,但是其实在很多场景下都能用它,接下来我们就来详细讲解。

Array.prototype.reduce()

语法

array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

参数描述
total必传。初始值, 或者计算结束后的返回值。
currentValue必传。当前元素
currentIndex可选。当前元素的索引
arr可选。当前元素所属的数组对象。
initialValue可选。传递给函数的初始值

提示: 如果不设置初始值(initialValue),reduce() 会自动使用数组的第一个元素作为初始值。不过为了可读性,我通常会显式地传入初始值。

场景示例(TypeScript)

1. 求和

Array.reduce() 最常见的用法之一就是计算数组元素的总和。例如,给定一个数字数组,求其所有元素的和:

const numbers: number[] = [1, 2, 3, 4, 5]; // 定义一个数字数组
const sum: number = numbers.reduce((acc, curr) => acc + curr, 0); 
// 使用 reduce 方法计算数组中所有数字的和,acc 是累加器,从 0 开始,curr 是当前值
console.log(sum); // 输出: 15

在这里Array.reduce() 的第一个参数是回调函数,它接受两个参数:累加器 (accumulator) acc 和当前元素 (current value) curr。第二个参数是初始值,在这里我们设置为 0。

2. 扁平化数组

如果我们有一个嵌套的二维数组,想要将其变成一个一维数组,我们可以使用Array.reduce()来实现。

const nestedArray: number[][] = [[1, 2], [3, 4], [5, 6]]; // 定义一个二维数组
const flattenedArray: number[] = nestedArray.reduce((acc, curr) => acc.concat(curr), []); 
// 使用 reduce 方法将二维数组扁平化,acc 是累加器,初始值是空数组,curr 是当前数组
console.log(flattenedArray); // 输出: [1, 2, 3, 4, 5, 6]

在这个例子中,我们使用了一个空数组作为初始累加器。然后在每次迭代中,使用 concat() 方法将当前的子数组加入到累加器上。最终得到一个完全扁平化的一维数组。

虽然 ES6 提供了 Array.flat() 方法也可以实现同样的功能,但是Array.reduce() 可以帮助我们在处理数组的同时进行其他操作。

3. 按照属性分组

假如你有一个对象数组,想要根据某个属性将它们进行分组,Array.reduce() 就是你最好的选择

interface Person { 
  name: string; // 定义人名属性
  age: number;  // 定义年龄属性
} //定义了一个 Person 接口,包含 name 和 age 两个属性,分别是字符串类型和数字类型。

const people: Person[] = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 25 },
  { name: 'Dave', age: 30 }
]; //定义了一个 Person 对象数组 people,其中包含四个人,每个人都有 name 和 age 属性。

const groupedByAge: { [key: number]: Person[] } = people.reduce((acc, curr) => {
  if (!acc[curr.age]) { // 如果累加器对象 acc 中不存在当前年龄 curr.age
    acc[curr.age] = []; // 为该年龄创建一个新的空数组acc[curr.age]。
  }
  acc[curr.age].push(curr); // 将当前 Person 对象 curr 添加到该年龄对应的数组中。
  return acc; // 返回累加器对象
}, {}); // 初始值是一个空对象

console.log(groupedByAge);
/*
输出:
{
  '25': [{ name: 'Alice', age: 25 }, { name: 'Charlie', age: 25 }],
  '30': [{ name: 'Bob', age: 30 }, { name: 'Dave', age: 30 }]
}
*/

这里,我们使用了一个对象作为初始累加器。在每次迭代中,我们检查累加器中是否存在当前元素的 age 属性对应的键。如果不存在,我们就为该 age 创建一个空数组。然后,我们将当前对象推入到对应的 age 数组中。最终得到的结果是一个对象,键是每个人的年龄,值是相同年龄的人组成的数组。

4. 创建查找映射 (Lookup Map)

假如我们要从数组中创建查找映射,我们也可以使用 reduce() ,这种方法在性能和代码可读性方面都很好。它可以替代的常用的find() 或 filter() 方法。

例如:我们希望通过产品的ID能快速的映射到我们对应的产品信息,但是我们的数据格式是对象数组格式的,这时我们就可以使用reduce()来生成一个能快速进行映射的数据格式。

interface Product {
  id: number;    // 定义产品ID属性
  name: string;  // 定义产品名称属性
  price: number; // 定义产品价格属性
}//定义了一个 Product 接口和一个包含多个 Product 对象的数组。

const products: Product[] = [
  { id: 1, name: 'Laptop', price: 999 },
  { id: 2, name: 'Phone', price: 699 },
  { id: 3, name: 'Tablet', price: 499 },
]; // 定义一个包含多个产品的数组

const productMap: { [key: number]: Product } = products.reduce((acc, curr) => {
  acc[curr.id] = curr; // 使用产品ID作为键,将产品对象存储在累加器对象中
  return acc; // 返回累加器对象
}, {}); // 初始值是一个空对象

console.log(productMap);
/*
输出:
{
  '1': { id: 1, name: 'Laptop', price: 999 },
  '2': { id: 2, name: 'Phone', price: 699 },
  '3': { id: 3, name: 'Tablet', price: 499 }
}
*/

// 通过ID访问产品
const laptop: Product = productMap[1];
console.log(laptop); // 输出: { id: 1, name: 'Laptop', price: 999 }

通过使用 Array.reduce()创建查找映射,我们可以通过唯一标识符以恒定的时间复杂度访问元素。无需再遍历数组来查找特定项

5. 统计元素出现次数

你是否也有遇到需要统计数组中每个元素出现的次数的情景?

Array.reduce() 也可以轻松的帮助我们解决问题。

const fruits: string[] = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']; // 定义一个字符串数组

const fruitCounts: { [key: string]: number } = fruits.reduce((acc, curr) => {
  acc[curr] = (acc[curr] || 0) + 1; // 如果当前水果在累加器对象中不存在,则设置为0,否则累加1
  return acc; // 返回累加器对象
}, {}); // 初始值是一个空对象

console.log(fruitCounts);
/*
输出:
{
  'apple': 3,
  'banana': 2,
  'orange': 1
}
*/

在这个例子中,我们初始化了一个空对象作为累加器。对于数组中的每个水果,我们检查它是否已经作为累加器对象中的属性存在。如果存在,则将其计数加 1;否则,将其初始化为 1。最终得到的结果会是一个对象,它可以让我们清晰的知道每个水果在数组中出现了多少次。

6. 组合函数

Array.reduce()也可以用于组合函数。我们可以用它来创建一个加工工厂,将数据一步步地进行加工处理。

const add5 = (x: number): number => x + 5;     // 定义一个函数,输入值加5
const multiply3 = (x: number): number => x * 3; // 定义一个函数,输入值乘3
const subtract2 = (x: number): number => x - 2; // 定义一个函数,输入值减2

const composedFunctions: ((x: number) => number)[] = [add5, multiply3, subtract2]; // 定义一个函数数组

const result: number = composedFunctions.reduce((acc, curr) => curr(acc), 10); 
// 使用 reduce 方法将函数依次应用到初始值 10 上,acc 是当前值,curr 是当前函数
console.log(result); // 输出: 43

在这个例子中,我们有一个函数数组,我们希望将它们按顺序对初始值 10进行运算。我们使用 Array.reduce() 遍历这些函数,并将每个函数计算的结果作为输入传递给下一个函数。最终等到的结果是将所有函数按顺序组合应用后的结果。

7. 实现简单的类 Redux 状态管理

你肯定不敢想像,Array.reduce()也能实现一个简单的 Redux 状态管理系统吧,下面我们就用Array.reduce()来实现一个简单的Redux 状态管理系统

interface State {
  count: number; // 定义计数器属性
  todos: string[]; // 定义待办事项数组属性
}

interface Action {
  type: string; // 定义动作类型属性
  payload?: any; // 定义可选的负载属性
}

const initialState: State = {
  count: 0,
  todos: [],
}; // 定义初始状态

const actions: Action[] = [
  { type: 'INCREMENT_COUNT' },//增加计数
  { type: 'ADD_TODO', payload: 'Learn Array.reduce()' },//添加一个待办事项
  { type: 'INCREMENT_COUNT' },//再次增加计数
  { type: 'ADD_TODO', payload: 'Master TypeScript' },//添加另一个待办事项
]; // 定义动作数组

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'INCREMENT_COUNT':
      return { ...state, count: state.count + 1 }; // 增加计数器
    case 'ADD_TODO':
      return { ...state, todos: [...state.todos, action.payload] }; // 添加待办事项
    default:
      return state; // 默认返回当前状态
  }
};//根据当前的状态和给定的动作来创建并返回一个新状态。它通过检查动作的`type`属性来决定如何更新状态。

const finalState: State = actions.reduce(reducer, initialState); 
// 使用 reduce 方法将动作数组依次应用到初始状态上
console.log(finalState);
/*
输出:
{
  count: 2,
  todos: ['Learn Array.reduce()', 'Master TypeScript']
}
*/

8. 去除重复项

你可能也遇到过这样的需求,有一个带有重复值的数组,我们需要去除数组里的重复项。Array.reduce() 可以轻松帮我们解决:

const numbers: number[] = [1, 2, 3, 2, 4, 3, 5, 1, 6]; // 定义一个包含重复值的数字数组

const uniqueNumbers: number[] = numbers.reduce((acc, curr) => {
  if (!acc.includes(curr)) { // 如果当前值不在累加器数组中
    acc.push(curr); // 将当前值添加到累加器数组中
  }
  return acc; // 返回累加器数组
}, []); // 初始值是一个空数组

console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5, 6]

在这里,我们初始化了一个空数组作为累加器。对于原始数组中的每个数字,我们使用 includes() 方法检查它是否已经存在于累加器中。如果不存在,则将其推入累加器数组。最终得到的结果是一个去除了数组里的重复项的数组。

9. 计算平均值

如果你想要计算一组数字的平均值?

reduce() 也可以做到!

const grades: number[] = [85, 90, 92, 88, 95]; // 定义一个包含多个成绩的数字数组

const average: number = grades.reduce((acc, curr, index, array) => {
  acc += curr; // 将当前值累加到累加器中
  if (index === array.length - 1) { // 如果是数组的最后一个元素
    return acc / array.length; // 计算累加器的平均值
  }
  return acc; // 返回累加器
}, 0); // 初始值是0

console.log(average); // 输出: 90

在这个例子中,我们将累加器初始化为 0。我们遍历每个成绩并将它添加到累加器中。当我们遍历到最后一个元素时(使用 index 和 array.length 进行检查),我们将累加器除以成绩总数就能得到平均值。

性能考虑

虽然Array.reduce()非常强大和灵活,但在处理数据很多的数组时或复杂操作时,有一个潜在的问题--性能消耗。因为Array.reduce()的每次迭代都会创建新的对象或数组,这样操作的结果会导致很多的性能和内存被消耗。

所以我们在使用Array.reduce()的同时也要注意性能的优化。

示例

const numbers: number[] = [1, 2, 3, 4, 5]; // 定义一个数字数组

const doubledNumbers: number[] = numbers.reduce((acc, curr) => {
  return [...acc, curr * 2]; // 返回一个新数组,包含当前值的两倍
}, []); // 初始值是一个空数组

console.log(doubledNumbers); // 输出: [2, 4, 6, 8, 10]

在这个例子中,我们在每次迭代中使用展开运算符 (...) 创建一个新数组,这可能会导致性能低下。我们可以使用 push()来 改变累加器数组,我们可以避免在每次迭代中创建新数组,从而获提高代码的性能。

优化:

// 定义一个只包含数字的数组
const numbers: number[] = [1, 2, 3, 4, 5];

// 使用reduce方法对numbers数组中的每个元素进行处理,以生成一个新的数组doubledNumbers
const doubledNumbers: number[] = numbers.reduce((acc, curr) => {
  // 在累加器acc(初始为一个空数组[])中推入当前元素curr乘以2的结果
  acc.push(curr * 2);
  // 返回累加器数组acc,作为下一次迭代的累加器值
  return acc;
}, []); // reduce方法的第二个参数[],这是累加器的初始值,代表一个空数组


console.log(doubledNumbers); // 输出: [2, 4, 6, 8, 10]

我们在使用Array.reduce()来处理对象数组也是一样的道理。

// 定义一个Person数组,每个Person具有名字和年龄属性
const people: Person[] = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 25 },
  { name: 'Dave', age: 30 }
];

// 使用reduce方法对people数组进行处理,以生成一个按年龄分组的对象groupedByAge
const groupedByAge: { [key: number]: Person[] } = people.reduce((acc, curr) => {
  // 检查累加器对象acc中是否已经有了当前元素年龄的键
  // 如果没有,初始化一个空数组作为该键的值,用于存储相同年龄的Person对象
  if (!acc[curr.age]) {
    acc[curr.age] = [];
  }
  // 将当前的Person对象添加到对应年龄的数组中
  acc[curr.age].push(curr);
  // 返回累加器对象acc,用于下一次迭代
  return acc;
}, {}); // reduce方法的初始值设为一个空对象{}

总结

其实Array.reduce()方法远比我们想象的要强大,只要我们有足够的想象力,Array.reduce()就能帮助我们解决很多难题。