数组去重、分组、扁平化
先理解原理和示例,再练习完整口述与追问。
回答要点
相等规则、业务主键、Map 安全分组、深度与循环。
原理拆解
去重先定义相等规则,分组先定义键,扁平化先定义层数与输入形状。Set 对原始值按集合规则去重,不会把两个相同字段对象视为同一实体;对象去重要显式使用业务键。扁平化迭代可避免极深递归栈,但仍需考虑循环数组与输出规模。
示例与推演
用 [1,1,NaN,NaN] 测原始值,用两个 id 相同但引用不同对象测业务去重;分组测空键和特殊字符串键;扁平化测空数组、不同深度与嵌套空位。每个示例同时说明输入是否改变和结果顺序是否保持。
易错点
不要把扁平化默认写成无限深又不处理循环引用。用普通对象存任意键时要注意 proto 等边界,可按需求选择 Map 或无原型字典。
参考回答可直接练习口述
去重先确认按值、对象引用还是业务 ID。Set 按其键相等规则去重,对象不会按内容自动合并。分组用 Map 可保留任意键类型,避免普通对象的原型键问题。扁平化用显式栈处理,保留顺序并支持深度,跳过稀疏空位;发现正在展开的数组形成循环时抛错,不无限循环。
function unique(values) {
return Array.from(new Set(values))
}
function uniqueBy(values, getKey) {
const seen = new Set()
return values.filter(value => {
const key = getKey(value)
if (seen.has(key)) return false
seen.add(key)
return true
})
}
function groupBy(values, getKey) {
const groups = new Map()
for (const value of values) {
const key = getKey(value)
if (!groups.has(key)) groups.set(key, [])
groups.get(key).push(value)
}
return groups
}
function flatten(input, depth = Infinity) {
if (!Array.isArray(input)) throw new TypeError('输入必须是数组')
if (depth !== Infinity && (!Number.isInteger(depth) || depth < 0)) {
throw new RangeError('depth 必须是非负整数或 Infinity')
}
const result = []
const active = new Set([input])
const stack = [{ array: input, index: 0, depth }]
while (stack.length) {
const frame = stack[stack.length - 1]
if (frame.index >= frame.array.length) {
active.delete(frame.array)
stack.pop()
continue
}
const index = frame.index++
if (!(index in frame.array)) continue
const value = frame.array[index]
if (Array.isArray(value) && frame.depth > 0) {
if (active.has(value)) throw new TypeError('数组存在循环引用')
active.add(value)
stack.push({ array: value, index: 0, depth: frame.depth - 1 })
} else result.push(value)
}
return result
}
示例:unique([1,1,NaN,NaN]) 为 [1,NaN];uniqueBy([{id:1},{id:1}], x => x.id) 保留第一条;flatten([1,[2,[3]]],1) 为 [1,2,[3]]。去重与分组平均 O(n),扁平化 O(实际访问元素数),辅助栈 O(展开深度),另计结果空间。没有修改输入数组。