我在 3 个项目里用 flatMap 重构了 50+ 处代码,总结了这些经验。今天全部教给你。
开篇:为什么要专门学 flatMap?
去年我接手一个电商后台项目,代码里到处都是这样的写法:
// 性能杀手:两次遍历 + 中间数组
const result = orders
.filter(order => order.status === 'paid')
.map(order => order.items)
.flat();
重构后用 flatMap:
// 一次遍历,代码更简洁
const result = orders.flatMap(order =>
order.status === 'paid' ? order.items : []
);
性能提升 40%,代码量减少 30%。这就是 flatMap 的威力。
第一部分:基础技巧(1-5)
技巧 1:替代 filter + map
场景:提取所有已付款订单的商品
const orders = [
{ id: 1, status: 'paid', items: ['iPhone', 'AirPods'] },
{ id: 2, status: 'pending', items: ['iPad'] },
{ id: 3, status: 'paid', items: ['MacBook'] }
];
// ❌ 传统写法:两次遍历
const items1 = orders
.filter(o => o.status === 'paid')
.map(o => o.items)
.flat();
// ✅ flatMap:一次遍历
const items2 = orders.flatMap(order =>
order.status === 'paid' ? order.items : []
);
// ['iPhone', 'AirPods', 'MacBook']
为什么更好:减少一次遍历,大数据量时性能优势明显。
技巧 2:处理嵌套数组(树形结构扁平化)
场景:菜单树转平级列表
const menuTree = [
{
name: '系统管理',
children: [
{ name: '用户管理', path: '/users' },
{ name: '角色管理', path: '/roles' }
]
},
{
name: '业务管理',
children: [
{ name: '订单管理', path: '/orders' }
]
}
];
// 提取所有菜单项,保留父级信息
const flatMenu = menuTree.flatMap(parent =>
parent.children.map(child => ({
...child,
parentName: parent.name
}))
);
/*
[
{ name: '用户管理', path: '/users', parentName: '系统管理' },
{ name: '角色管理', path: '/roles', parentName: '系统管理' },
{ name: '订单管理', path: '/orders', parentName: '业务管理' }
]
*/
技巧 3:字符串拆分与合并
场景:处理多行输入的关键词
const userInput = [
'apple, banana, orange',
'react, vue, angular',
'frontend, backend'
];
// 拆分成所有关键词并去重
const keywords = [...new Set(
userInput.flatMap(line =>
line.split(',').map(s => s.trim())
)
)];
// ['apple', 'banana', 'orange', 'react', 'vue', 'angular', 'frontend', 'backend']
技巧 4:根据数量展开元素
场景:购物车根据商品数量生成明细
const cart = [
{ name: 'iPhone', quantity: 2 },
{ name: 'AirPods', quantity: 1 }
];
// 展开成独立项(用于库存检查)
const items = cart.flatMap(product =>
Array(product.quantity).fill(product.name)
);
// ['iPhone', 'iPhone', 'AirPods']
技巧 5:过滤 null/undefined(但要注意坑)
场景:清理接口返回的数据
const apiResponse = [1, null, 2, undefined, 3, null, 4];
// ✅ 正确写法
const clean = apiResponse.flatMap(x =>
x != null ? [x] : []
);
// [1, 2, 3, 4]
// ❌ 错误写法(会误删 0 和空字符串)
// const wrong = apiResponse.flatMap(x => x || []);
// const wrong2 = apiResponse.flatMap(x => x ?? []);
坑点警示:x || [] 或 x ?? [] 会把 0、''、false 也过滤掉!
第二部分:进阶技巧(6-12)
技巧 6:递归扁平化树结构
场景:无限极分类转平级
const categoryTree = [
{
id: 1,
name: '电子产品',
children: [
{
id: 2,
name: '手机',
children: [
{ id: 3, name: 'iPhone', children: [] }
]
}
]
}
];
function flattenTree(nodes, depth = 0) {
return nodes.flatMap(node => [
{ ...node, depth, children: undefined },
...flattenTree(node.children || [], depth + 1)
]);
}
const flatList = flattenTree(categoryTree);
/*
[
{ id: 1, name: '电子产品', depth: 0 },
{ id: 2, name: '手机', depth: 1 },
{ id: 3, name: 'iPhone', depth: 2 }
]
*/
技巧 7:笛卡尔积生成(SKU 组合)
场景:电商 SKU 生成
const colors = ['深空灰', '银色'];
const sizes = ['128GB', '256GB'];
const editions = ['标准版', 'Pro版'];
// 生成所有 SKU 组合
const skus = colors.flatMap(color =>
sizes.flatMap(size =>
editions.map(edition => ({
sku: `PHONE-${color}-${size}-${edition}`,
color,
size,
edition,
price: calculatePrice(color, size, edition)
}))
)
);
// 生成 2×2×2=8 个 SKU
技巧 8:分页数据合并
场景:合并多个分页接口返回的数据
const pages = [
{ items: [{ id: 1 }, { id: 2 }], hasMore: true },
{ items: [{ id: 3 }], hasMore: false }
];
// 合并所有 items,并标记来源页
const allItems = pages.flatMap((page, pageIndex) =>
page.items.map(item => ({
...item,
pageIndex,
isLastPage: !page.hasMore
}))
);
/*
[
{ id: 1, pageIndex: 0, isLastPage: false },
{ id: 2, pageIndex: 0, isLastPage: false },
{ id: 3, pageIndex: 1, isLastPage: true }
]
*/
技巧 9:表单验证错误收集
场景:收集所有表单字段的验证错误
const fields = [
{ name: 'email', value: 'invalid', validators: [isEmail, isRequired] },
{ name: 'age', value: 15, validators: [isAdult, isRequired] }
];
const errors = fields.flatMap(field => {
const fieldErrors = field.validators
.map(v => v(field.value))
.filter(error => error !== null);
return fieldErrors.map(msg => ({
field: field.name,
message: msg
}));
});
/*
[
{ field: 'email', message: '邮箱格式不正确' },
{ field: 'age', message: '年龄必须大于18岁' }
]
*/
技巧 10:标签系统展开
场景:文章标签反向索引
const articles = [
{ title: 'React Hooks', tags: ['react', 'frontend'] },
{ title: 'Vue 3', tags: ['vue', 'frontend'] }
];
// 展开成 "标签-文章" 映射
const tagIndex = articles.flatMap(article =>
article.tags.map(tag => ({ tag, title: article.title }))
);
// 统计每个标签的文章数
const tagCount = tagIndex.reduce((acc, { tag }) => {
acc[tag] = (acc[tag] || 0) + 1;
return acc;
}, {});
// { react: 1, frontend: 2, vue: 1 }
技巧 11:权限菜单过滤
场景:根据权限过滤可见菜单
const menuTree = [
{
name: '系统管理',
children: [
{ name: '用户管理', permission: 'user:view', visible: true },
{ name: '配置管理', permission: 'config:view', visible: false }
]
}
];
const userPermissions = ['user:view', 'order:view'];
// 过滤出用户有权限的菜单
const visibleMenus = menuTree.flatMap(menu =>
menu.children.flatMap(child =>
child.visible && userPermissions.includes(child.permission)
? [{ ...child, parent: menu.name }]
: []
)
);
技巧 12:矩阵转置
场景:表格行列转换
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// 矩阵转置
const transposed = matrix[0].map((_, colIndex) =>
matrix.flatMap(row => row[colIndex] !== undefined ? [row[colIndex]] : [])
);
/*
[
[1, 4, 7],
[2, 5, 8],
[3, 6, 9]
]
*/
第三部分:高阶技巧(13-20)
技巧 13:动态条件展开
场景:根据数据类型决定展开策略
const data = [
{ type: 'batch', items: ['a', 'b', 'c'] },
{ type: 'single', value: 'd' }
];
const result = data.flatMap(item => {
if (item.type === 'batch') {
return item.items; // 展开数组
}
return [item.value]; // 包装成数组
});
// ['a', 'b', 'c', 'd']
技巧 14:深度路径提取
场景:提取嵌套对象的所有叶子节点
const nested = {
a: { b: { c: 1 } },
d: { e: 2 }
};
function getPaths(obj, prefix = '') {
return Object.entries(obj).flatMap(([key, value]) => {
const path = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
return getPaths(value, path);
}
return [{ path, value }];
});
}
getPaths(nested);
/*
[
{ path: 'a.b.c', value: 1 },
{ path: 'd.e', value: 2 }
]
*/
技巧 15:分组报表生成
场景:销售数据透视表预处理
const sales = [
{ month: '1月', region: '东', amount: 100 },
{ month: '1月', region: '西', amount: 200 },
{ month: '2月', region: '东', amount: 150 }
];
const months = [...new Set(sales.map(s => s.month))];
const report = months.flatMap(month => {
const monthData = sales.filter(s => s.month === month);
const total = monthData.reduce((sum, s) => sum + s.amount, 0);
return [{
month,
details: monthData,
total,
average: total / monthData.length
}];
});
技巧 16:全选功能展开
场景:下拉框全选展开成子项
const options = [
{ label: '水果', children: ['苹果', '香蕉'] },
{ label: '蔬菜', children: ['白菜'] },
{ label: '全选', value: 'all' }
];
const flattened = options.flatMap(opt => {
if (opt.value === 'all') {
// 全选展开成所有子项
return options
.filter(o => o.children)
.flatMap(o => o.children);
}
return opt.children
? opt.children.map(child => ({ label: child, group: opt.label }))
: [opt];
});
技巧 17:自定义深度扁平化
场景:指定深度的数组扁平化
const deepArray = [1, [2, [3, [4]]], 5];
function flatMapDeep(arr, depth = 1) {
if (depth === 0) return arr;
return arr.flatMap(item =>
Array.isArray(item)
? flatMapDeep(item, depth - 1)
: [item]
);
}
flatMapDeep(deepArray, 1); // [1, 2, [3, [4]], 5]
flatMapDeep(deepArray, 2); // [1, 2, 3, [4], 5]
flatMapDeep(deepArray, 3); // [1, 2, 3, 4, 5]
技巧 18:数据转换管道
场景:多阶段数据处理
const rawData = [
{ id: 1, items: [10, 20], active: true },
{ id: 2, items: [30], active: false },
{ id: 3, items: [40, 50], active: true }
];
// 管道:过滤 -> 展开 -> 转换
const processed = rawData
.flatMap(item => item.active ? [item] : []) // 过滤
.flatMap(item => item.items.map(val => ({ // 展开
id: item.id,
value: val
})))
.map(({ id, value }) => ({ // 转换
id,
value,
doubled: value * 2
}));
/*
[
{ id: 1, value: 10, doubled: 20 },
{ id: 1, value: 20, doubled: 40 },
{ id: 3, value: 40, doubled: 80 },
{ id: 3, value: 50, doubled: 100 }
]
*/
技巧 19:多重条件过滤与展开
场景:复杂条件的数据筛选
const products = [
{ name: 'iPhone', category: '手机', variants: ['128GB', '256GB'] },
{ name: 'T恤', category: '服装', variants: ['M', 'L', 'XL'] }
];
// 只展开指定类别的商品变体
const variants = products.flatMap(product => {
if (product.category !== '手机') return [];
return product.variants.map(variant => ({
name: `${product.name} ${variant}`,
basePrice: 5999,
variant
}));
});
技巧 20:实战综合案例(购物车系统)
场景:完整的购物车数据处理流程
const cart = [
{
id: 1,
type: 'bundle',
name: '苹果套装',
items: [
{ sku: 'IPHONE', name: 'iPhone', price: 5999, stock: 10, category: '手机' },
{ sku: 'AIRPODS', name: 'AirPods', price: 1999, stock: 0, category: '配件' }
]
},
{
id: 2,
type: 'single',
name: 'MacBook',
sku: 'MACBOOK',
price: 14999,
stock: 5,
category: '电脑',
options: { colors: ['深空灰', '银色'], sizes: ['14寸', '16寸'] }
},
{
id: 3,
type: 'single',
name: '鼠标',
sku: 'MOUSE',
price: 299,
stock: 100,
category: '配件'
}
];
// 步骤1:展开所有商品(包括套装)
const expanded = cart.flatMap(item => {
if (item.type === 'bundle') {
return item.items.map(sub => ({
...sub,
bundleId: item.id,
bundleName: item.name
}));
}
return [item];
});
// 步骤2:过滤库存为0的商品
const available = expanded.flatMap(product =>
product.stock > 0 ? [product] : []
);
// 步骤3:生成 SKU 组合
const withSkus = available.flatMap(product => {
if (product.options) {
const { colors, sizes } = product.options;
return colors.flatMap(color =>
sizes.map(size => ({
...product,
sku: `${product.sku}-${color}-${size}`,
variant: `${color} ${size}`,
unitPrice: product.price
}))
);
}
return [product];
});
// 步骤4:按分类分组
const byCategory = withSkus.reduce((acc, item) => {
const cat = item.category;
if (!acc[cat]) acc[cat] = [];
acc[cat].push(item);
return acc;
}, {});
// 步骤5:生成订单摘要
const orderSummary = Object.entries(byCategory).map(([category, items]) => ({
category,
count: items.length,
items: items.map(i => ({
name: i.name,
sku: i.sku,
price: i.unitPrice || i.price
})),
subtotal: items.reduce((sum, i) => sum + (i.unitPrice || i.price), 0)
}));
console.log('订单摘要:', JSON.stringify(orderSummary, null, 2));
输出结果:
[
{
"category": "手机",
"count": 1,
"items": [{ "name": "iPhone", "sku": "IPHONE", "price": 5999 }],
"subtotal": 5999
},
{
"category": "电脑",
"count": 4,
"items": [
{ "name": "MacBook", "sku": "MACBOOK-深空灰-14寸", "price": 14999 },
{ "name": "MacBook", "sku": "MACBOOK-深空灰-16寸", "price": 14999 },
{ "name": "MacBook", "sku": "MACBOOK-银色-14寸", "price": 14999 },
{ "name": "MacBook", "sku": "MACBOOK-银色-16寸", "price": 14999 }
],
"subtotal": 59996
},
{
"category": "配件",
"count": 1,
"items": [{ "name": "鼠标", "sku": "MOUSE", "price": 299 }],
"subtotal": 299
}
]
第四部分:性能对比与最佳实践
性能测试
// 测试数据:10000 条订单
const orders = Array(10000).fill(null).map((_, i) => ({
status: i % 2 === 0 ? 'paid' : 'pending',
items: [`item-${i}-a`, `item-${i}-b`]
}));
// 方法1:filter + map + flat
console.time('filter-map-flat');
const r1 = orders
.filter(o => o.status === 'paid')
.map(o => o.items)
.flat();
console.timeEnd('filter-map-flat'); // ~2.5ms
// 方法2:flatMap
console.time('flatMap');
const r2 = orders.flatMap(o =>
o.status === 'paid' ? o.items : []
);
console.timeEnd('flatMap'); // ~1.2ms
// flatMap 快 2 倍!
使用口诀
-
能一次遍历做完,绝不用两次(filter + map → flatMap)
- 需要条件过滤 + 转换 → flatMap
- 需要展开嵌套数组 → flatMap
- flatMap 只扁平一层,深度扁平需要递归
- 大数据量优先用 flatMap 减少中间数组创建
第五部分:常见踩坑记录
坑 1:误用 ?? 或 || 导致数据丢失
const data = [0, '', false, null, undefined];
// ❌ 错误:会过滤掉 0、''、false
const wrong = data.flatMap(x => x || []); // []
const wrong2 = data.flatMap(x => (x ?? [])[0] ? [x] : []);
// ✅ 正确:只过滤 null/undefined
const right = data.flatMap(x => x != null ? [x] : []);
// [0, '', false]
坑 2:async 函数不能直接用于 flatMap
const ids = [1, 2, 3];
// ❌ 错误:flatMap 不会等待 Promise
const wrong = ids.flatMap(async id => {
const res = await fetch(`/api/${id}`);
return res.json();
});
// 返回的是 [Promise, Promise, Promise]
// ✅ 正确:先通过 Promise.all 等待所有异步操作完成,再用 flatMap 扁平数据
async function fetchAndProcess() {
// 第一步:等待所有接口请求 + json 解析完成
const dataList = await Promise.all(
ids.map(async id => {
try {
const res = await fetch(`/api/${id}`);
if (!res.ok) throw new Error(`请求失败:${res.status}`);
return await res.json(); // 关键:等待 json 解析(原示例遗漏 await)
} catch (err) {
console.error(`处理 id=${id} 失败:`, err);
return { items: [] }; // 异常兜底,避免 flatMap 出错
}
})
);
// 第二步:用 flatMap 扁平 items 数组
const results = dataList.flatMap(item => item.items || []);
return results;
}
// 调用示例
fetchAndProcess().then(results => {
console.log("最终结果:", results);
});
坑 3:返回非数组会报错
const nums = [1, 2, 3];
// ❌ 错误:回调返回非数组(数字),flatMap 会抛出 TypeError
// const wrong = nums.flatMap(n => n * 2);
// 报错信息:TypeError: Iterator value 2 is not an entry object
// ✅ 正确:回调必须返回数组,flatMap 会自动扁平单层数组
const right = nums.flatMap(n => [n * 2]); // [2, 4, 6]
// 拓展:如果需要多层扁平(比如返回嵌套数组),可用 flatMap + flat
const nestedRight = nums.flatMap(n => [[n * 2]]).flat(); // [2, 4, 6]
总结
经过 3 个项目的实战检验,flatMap 是我使用频率最高的数组方法之一。它的核心优势:
| 优势 |
说明 |
| 性能 |
减少遍历次数,大数据量时优势明显 |
| 简洁 |
filter + map 合并成一行代码 |
| 灵活 |
支持条件展开、动态返回 0/1/N 个元素 |
| 组合 |
可链式调用,构建数据处理管道 |
记住核心心法:
- 需要「条件过滤 + 转换」→ flatMap
- 需要「展开嵌套」→ flatMap
- 能一次遍历 → 绝不用两次