大家都知道数组有一个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()
就能帮助我们解决很多难题。