跳到主要内容

扁平化嵌套数组与对象

问题

问题一:扁平化嵌套数组

给定一个可能包含多层嵌套的数组,将其扁平化为一维数组。

输入:[1, [2, [3, [4]], 5]]
输出:[1, 2, 3, 4, 5]

问题二:扁平化嵌套对象

给定一个嵌套的对象,将其扁平化,用 . 连接键名。

输入:{ a: { b: { c: 1 }, d: 2 }, e: 3 }
输出:{ 'a.b.c': 1, 'a.d': 2, 'e': 3 }
前端高频

这两个问题在前端面试中出现频率极高——数组扁平化考察递归基础,对象扁平化考察实际工程能力(API 数据处理、表单数据转换等)。

答案 - 数组扁平化

方法一:递归

flattenArray.ts
function flatten(arr: any[]): any[] {
const result: any[] = [];

for (const item of arr) {
if (Array.isArray(item)) {
result.push(...flatten(item));
} else {
result.push(item);
}
}

return result;
}

// 支持指定展开深度
function flattenDepth(arr: any[], depth: number = Infinity): any[] {
if (depth <= 0) return [...arr];

return arr.reduce((acc, item) => {
if (Array.isArray(item)) {
return acc.concat(flattenDepth(item, depth - 1));
}
return acc.concat(item);
}, []);
}

方法二:迭代(栈)

flattenIterative.ts
function flatten(arr: any[]): any[] {
const stack = [...arr]; // 浅拷贝
const result: any[] = [];

while (stack.length > 0) {
const item = stack.pop()!;
if (Array.isArray(item)) {
stack.push(...item); // 展开后放回栈
} else {
result.unshift(item); // 保持顺序
}
}

return result;
}

方法三:原生 API

flattenNative.ts
// ES2019 Array.prototype.flat
const result = [1, [2, [3, [4]]]].flat(Infinity);
// [1, 2, 3, 4]

// 指定深度
[1, [2, [3]]].flat(1); // [1, 2, [3]]

// toString hack(仅限数字数组)
const hack = [1, [2, [3]]].toString().split(',').map(Number);

答案 - 对象扁平化

方法一:递归(推荐)

flattenObject.ts
function flattenObject(
obj: Record<string, any>,
prefix: string = '',
result: Record<string, any> = {}
): Record<string, any> {
for (const key of Object.keys(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
const value = obj[key];

if (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value)
) {
// 递归处理嵌套对象
flattenObject(value, newKey, result);
} else {
result[newKey] = value;
}
}

return result;
}

// 测试
flattenObject({ a: { b: { c: 1 }, d: 2 }, e: 3 });
// { 'a.b.c': 1, 'a.d': 2, 'e': 3 }

方法二:支持数组下标

flattenObjectArray.ts
function flattenObject(
obj: any,
prefix: string = '',
result: Record<string, any> = {}
): Record<string, any> {
if (typeof obj !== 'object' || obj === null) {
result[prefix] = obj;
return result;
}

if (Array.isArray(obj)) {
obj.forEach((item, index) => {
flattenObject(item, `${prefix}[${index}]`, result);
});
} else {
for (const key of Object.keys(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
flattenObject(obj[key], newKey, result);
}
}

return result;
}

// 测试
flattenObject({
a: [1, { b: 2 }],
c: { d: 3 }
});
// { 'a[0]': 1, 'a[1].b': 2, 'c.d': 3 }

反向操作:展开对象

unflattenObject.ts
function unflattenObject(obj: Record<string, any>): Record<string, any> {
const result: Record<string, any> = {};

for (const [key, value] of Object.entries(obj)) {
const keys = key.split('.');
let current = result;

for (let i = 0; i < keys.length - 1; i++) {
if (!(keys[i] in current)) {
current[keys[i]] = {};
}
current = current[keys[i]];
}

current[keys[keys.length - 1]] = value;
}

return result;
}

// 测试
unflattenObject({ 'a.b.c': 1, 'a.d': 2, 'e': 3 });
// { a: { b: { c: 1 }, d: 2 }, e: 3 }

常见面试追问

Q1: 如何处理循环引用?

答案:用 WeakSet 记录已访问的对象:

function flattenSafe(
obj: Record<string, any>,
prefix: string = '',
result: Record<string, any> = {},
seen: WeakSet<object> = new WeakSet()
): Record<string, any> {
if (seen.has(obj)) return result; // 跳过循环引用
seen.add(obj);

for (const key of Object.keys(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
const value = obj[key];

if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
flattenSafe(value, newKey, result, seen);
} else {
result[newKey] = value;
}
}

return result;
}

Q2: 前端中的实际应用场景?

答案

  • 表单数据处理:将嵌套的表单数据转为扁平键值对提交
  • API 响应转换:将深层嵌套的 API 数据展平便于表格展示
  • 国际化 key{ common: { button: { save: '保存' } } }{ 'common.button.save': '保存' }
  • 配置合并:dotenv 格式的配置 DATABASE.HOST → 嵌套对象

Q3: flat() 的 polyfill?

答案

if (!Array.prototype.flat) {
Array.prototype.flat = function (depth: number = 1): any[] {
return depth > 0
? this.reduce(
(acc: any[], item: any) =>
acc.concat(Array.isArray(item) ? item.flat(depth - 1) : item),
[]
)
: [...this];
};
}

相关链接