做前端开发的同学,八成都写过 arr.filter().map() 这样的链式调用,也用 reduce 求过数组总和。但如果你只把它们当“高级for循环”用,就像把瑞士军刀拧成了锤子——暴殄天物啊!
今天咱们跳出基础用法,聚焦90%业务场景都能用的实战技巧,从筛选逻辑到数据聚合,再到性能优化,把这两个数组方法的价值拉满。所有代码都能直接复制到控制台运行,建议先收藏再看~
一、filter:筛选的艺术,不止“是或否”
提到 filter,多数人第一反应是“按条件留元素”。但新手常踩两个坑:逻辑冗余和性能浪费,先看基础用法再拆进阶技巧。
1. 基础:精准筛选的核心逻辑
filter的核心是“回调返回true则保留”,它不会修改原数组,而是返回新数组——这一点在函数式编程里特别重要。
// 场景1:筛选成年用户
const users = [
{ id: 1, name: '阿杰', age: 17 },
{ id: 2, name: '阿花', age: 22 },
{ id: 3, name: '阿强', age: 19 }
];
// 筛选age>18的用户,保留id和name字段
const adults = users.filter(user => user.age > 18);
console.log(adults); // [{id:2,...}, {id:3,...}]
2. 进阶:避免“筛选+转换”的二次遍历
新手常写 filter().map(),但这样会遍历数组两次。如果数据量超过10万条,性能差异会非常明显。此时可以用“筛选后直接处理”的逻辑优化:
// 优化前:两次遍历
const adultNames = users
.filter(user => user.age > 18)
.map(user => user.name);
// 优化后:一次遍历(用reduce合并逻辑,后面细讲)
const adultNames = users.reduce((acc, user) => {
if (user.age > 18) acc.push(user.name);
return acc;
}, []);
3. 避坑:空值处理与边界条件
筛选字符串或对象时,一定要处理空值,否则容易出现Cannot read property 'xxx' of undefined的报错:
// 筛选非空且长度>3的用户名
const names = ['', 'Tom', undefined, 'Jerry', null, 'Bob'];
// 先判断存在性,再判断长度
const validNames = names.filter(name =>
typeof name === 'string' && name.trim().length > 3
);
console.log(validNames); // ['Jerry']
二、reduce:被低估的“数据处理瑞士军刀”
如果说filter是“单功能筛选器”,reduce就是“全能处理器”。它的核心是“折叠”——把数组降维成任意类型的值,从数字到对象,再到函数,全能搞定。
1. 基础:累积计算的正确姿势
求和、求积这些基础用法就不说了,重点提醒:一定要传初始值!否则当数组为空时会报错,且初始值决定了累加器的类型。
const numbers = [1, 2, 3, 4];
// 错误:无初始值,空数组会抛错
numbers.reduce((acc, curr) => acc + curr);
// 正确:传初始值0,明确累加器为数字类型
const sum = numbers.reduce((acc, curr) => acc + curr, 0); // 10
// 进阶:求对象数组的某字段总和
const totalAge = users.reduce((acc, user) => acc + user.age, 0); // 58
2. 实战:比for循环更优雅的8个场景
这部分是重点,每个场景都附“业务需求+代码实现”,直接对应工作中的实际问题:
场景1:数组去重(支持原始值和对象)
// 原始值去重
const uniqueNums = [1, 2, 2, 3].reduce((acc, curr) => {
return acc.includes(curr) ? acc : [...acc, curr];
}, []); // [1,2,3]
// 对象去重(按id)
const uniqueUsers = users.reduce((acc, curr) => {
// 用对象存已出现的id,比findIndex效率高
if (!acc.map.has(curr.id)) {
acc.list.push(curr);
acc.map.add(curr.id);
}
return acc;
}, { list: [], map: new Set() }).list;
场景2:对象分组(高频需求)
比如把用户按“年龄段”分组,传统for循环要写嵌套逻辑,reduce一行核心代码搞定:
// 按年龄段分组:18以下、18-30、30以上
const groupedUsers = users.reduce((acc, curr) => {
let key;
if (curr.age < 18) key = 'teen';
else if (curr.age <= 30) key = 'young';
else key = 'adult';
// 若该分组不存在则初始化数组
acc[key] = acc[key] || [];
acc[key].push(curr);
return acc;
}, {});
// 结果:{ teen: [...], young: [...], adult: [...] }
场景3:统计元素频率(词频统计)
// 统计字符串中各字符出现次数
const str = 'hello world';
const charCount = str.split('').reduce((acc, curr) => {
// 短路运算:不存在则设为0,再+1
acc[curr] = (acc[curr] || 0) + 1;
return acc;
}, {}); // { h:1, e:1, l:3, o:2, ' ':1, w:1, r:1, d:1 }
场景4:数组扁平化(一级)
const nestedArr = [1, [2, 3], [4, 5]];
const flatArr = nestedArr.reduce((acc, curr) => {
// 区分数组和非数组元素
return Array.isArray(curr) ? [...acc, ...curr] : [...acc, curr];
}, []); // [1,2,3,4,5]
场景5:异步函数串行执行
Promise.all是并行执行,而reduce能实现“前一个异步完成再执行下一个”,比如批量接口请求:
// 模拟异步请求
const fetchUser = (id) => fetch(`/api/user/${id}`).then(res => res.json());
// 按顺序请求id为1、2、3的用户
[1,2,3].reduce(async (prev, curr) => {
// 等待前一个请求完成
const prevRes = await prev;
// 执行当前请求
const currRes = await fetchUser(curr);
// 收集结果
return [...prevRes, currRes];
}, Promise.resolve([]));
场景6:实现函数管道(pipe)
把多个函数组合成一个管道,输入值依次经过每个函数处理,这是函数式编程的核心技巧:
// 定义单个处理函数
const double = x => x * 2;
const add1 = x => x + 1;
const square = x => x ** 2;
// 实现管道函数
const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x);
// 组合:先乘2 → 加1 → 平方
const calculate = pipe(double, add1, square);
calculate(3); // (3*2+1)² = 49
场景7:URL查询参数拼接
// 把对象转成query字符串
const toQuery = obj =>
Object.entries(obj)
.reduce((str, [k, v], i) => {
// 第一个参数不加&,后续加
const separator = i === 0 ? '' : '&';
// 处理中文等特殊字符
return `${str}${separator}${k}=${encodeURIComponent(v)}`;
}, '');
toQuery({ name: '前端阿杰', age: 28 });
// "name=%E5%89%8D%E7%AB%AF%E9%98%BF%E6%9D%B0&age=28"
场景8:替代filter+map(性能优化)
回到开头的问题,用reduce合并筛选和转换逻辑,减少一次遍历:
// 需求:筛选成年用户,只保留name字段
const adultNames = users.reduce((acc, user) => {
if (user.age > 18) acc.push(user.name);
return acc;
}, []); // ['阿花', '阿强']
3. 黑科技:reduceRight逆向折叠
reduce是“从左到右”折叠,reduceRight则是“从右到左”,适合处理需要逆向逻辑的场景,比如构造嵌套对象:
// 把['a','b','c']转成{a:{b:{c:123}}}
const nestPath = (keys, value) =>
keys.reduceRight((acc, key) => ({ [key]: acc }), value);
nestPath(['a', 'b', 'c'], 123); // {a:{b:{c:123}}}
三、性能优化:这些细节能省50%时间
方法用对是基础,用得快才是本事。结合大数据场景的测试结论,分享3个关键优化点:
1. 优先用reduce合并多步操作
filter+map会遍历两次数组,而reduce只遍历一次。在10万条数据的测试中,reduce的耗时约为filter+map的55%。
2. 避免在回调中创建新对象
比如数组去重时,用Set存已出现的ID比用数组includes效率高——因为includes是O(n)复杂度,Set的has是O(1):
// 高效去重(用Set存标识)
const uniqueUsers = users.reduce((acc, curr) => {
if (!acc.idSet.has(curr.id)) {
acc.list.push(curr);
acc.idSet.add(curr.id);
}
return acc;
}, { list: [], idSet: new Set() }).list;
3. 大数据用原始类型或避免中间数组
如果是纯数字数组,优先用reduce而非Math.max(...arr)——因为展开数组会触发二次遍历,数据量越大差异越明显:
// 10万条数字找最大值
const largeArr = Array.from({ length: 100000 }, () => Math.random());
// 高效:一次遍历
const max = largeArr.reduce((acc, curr) => Math.max(acc, curr), -Infinity);
// 低效:先展开再遍历
const max = Math.max(...largeArr);
四、总结:怎么选?一张表讲清楚
| 方法 | 核心能力 | 返回值 | 最佳场景 |
|---|---|---|---|
| filter | 按条件筛选元素 | 新数组(元素子集) | 简单筛选,无需后续转换 |
| reduce | 累积计算、数据转换 | 任意类型(数字/对象/函数等) | 求和、分组、去重、合并逻辑 |
心法总结:filter是“减法”,只留需要的元素;reduce是“变法”,能把数组改成任何你想要的样子。日常开发中,先想清楚是“单纯筛选”还是“需要处理转换”,再决定用哪个——能一次遍历搞定的,绝不走两次流程。
用reduce实现多级flat
核心思路:利用reduce遍历数组,遇到子数组则递归调用自身展开,非数组元素直接存入累加器,通过depth参数控制扁平化层级(默认1级,传Infinity实现完全扁平化)。
// 实现支持多级扁平化的flat方法
const myFlat = (arr, depth = 1) => {
// 递归终止条件:depth为0时直接返回当前数组
if (depth <= 0) return [...arr];
return arr.reduce((acc, curr) => {
// 若当前元素是数组且未达到目标深度,递归展开
if (Array.isArray(curr)) {
acc.push(...myFlat(curr, depth - 1));
} else {
// 非数组元素直接存入累加器
acc.push(curr);
}
return acc;
}, []);
};
// 测试用例
const testArr1 = [1, [2, [3, [4]]]];
console.log(myFlat(testArr1)); // [1,2,[3,[4]]](默认1级)
console.log(myFlat(testArr1, 2)); // [1,2,3,[4]](2级)
console.log(myFlat(testArr1, Infinity)); // [1,2,3,4](完全扁平化)
console.log(myFlat(testArr1, 0)); // [1,[2,[3,[4]]]](深度0,不展开)
关键细节:用[...arr]浅拷贝避免修改原数组,通过push(...)合并展开后的子数组,递归时将depth减1确保层级可控,逻辑与原生Array.prototype.flat完全对齐。
觉得有用的话,点赞收藏走一波,下次遇到数据处理难题直接翻出来用!