数组去重、分组、扁平化

先理解原理和示例,再练习完整口述与追问。

相等规则、业务主键、Map 安全分组、深度与循环。

去重先定义相等规则,分组先定义键,扁平化先定义层数与输入形状。Set 对原始值按集合规则去重,不会把两个相同字段对象视为同一实体;对象去重要显式使用业务键。扁平化迭代可避免极深递归栈,但仍需考虑循环数组与输出规模。

用 [1,1,NaN,NaN] 测原始值,用两个 id 相同但引用不同对象测业务去重;分组测空键和特殊字符串键;扁平化测空数组、不同深度与嵌套空位。每个示例同时说明输入是否改变和结果顺序是否保持。

不要把扁平化默认写成无限深又不处理循环引用。用普通对象存任意键时要注意 proto 等边界,可按需求选择 Map 或无原型字典。

去重先确认按值、对象引用还是业务 ID。Set 按其键相等规则去重,对象不会按内容自动合并。分组用 Map 可保留任意键类型,避免普通对象的原型键问题。扁平化用显式栈处理,保留顺序并支持深度,跳过稀疏空位;发现正在展开的数组形成循环时抛错,不无限循环。

js
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(展开深度),另计结果空间。没有修改输入数组。