JS 数组去重的 6 种写法:原始值 Set 一行搞定,对象数组按 key 去重要用 Map

数组去重最常翻车的场景:对象数组用 Set 去不掉——Set 按引用比较,两个内容相同的对象也不相等。本文给出原始值一行去重、对象数组按 key 去重的 Map 写法、保留首次出现项的 filter 变体,附六种方法决策表与「去不掉」的排查清单。

约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证

先说结论:原始值用 Set,对象数组按 key 用 Map

90% 的去重需求一行 [...new Set(arr)] 就够;剩下 10% 是对象数组,Set 在这里永远「去不掉」,原因和修复见下文。

原始值去重:一行搞定
const nums = [1, 2, 2, 3, 'a', 'a', null, null];
const unique = [...new Set(nums)];
console.log(unique); // [1, 2, 3, 'a', null] —— NaN 也能去重

为什么对象数组用 Set 去不掉

Set 内部按 SameValueZero 比较,对对象来说就是比引用。两个内容完全一样的对象字面量也是两个不同引用,所以都会被保留。

  1. 对两个「重复」元素跑 JSON.stringify(a) === JSON.stringify(b),结果为 true 就说明内容相同但引用不同——这正是 Set 放行它们的原因。
  2. 你真正想比的是「业务上的唯一标识」,比如 id,而不是对象本身。
  3. 所以正确思路是:以 key 为索引建 Map,对象作为值,最后取 .values()。

对象数组按 key 去重:Map 写法

按 id 去重,后出现的覆盖先出现的
function uniqueBy(arr, key) {
  return [...new Map(arr.map(item => [item[key], item])).values()];
}

const users = [
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
  { id: 1, name: '王五' },
];
console.log(uniqueBy(users, 'id'));
// [{ id: 1, name: '王五' }, { id: 2, name: '李四' }]
// 注意:id=1 保留的是「最后出现」的;想保留第一个看下一段

想保留第一次出现的对象:filter + Set

一行变体:遍历时用 Set 记录见过的 key,重复的直接跳过。这个写法还天然保留原数组顺序。

keep-first 版本
function uniqueByFirst(arr, key) {
  const seen = new Set();
  return arr.filter(item => {
    if (seen.has(item[key])) return false;
    seen.add(item[key]);
    return true;
  });
}
// uniqueByFirst(users, 'id') → [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]

六种写法决策表:什么时候用哪个

先看数据里有没有对象,再看要不要保留首次出现,30 秒选完。

写法适用场景注意点
[...new Set(arr)]原始值数组(含 NaN、null)对象数组无效
filter + indexOf要兼容极老环境O(n²),大数据慢;NaN 去不掉
Map 按 key对象数组按唯一字段去重后出现的覆盖先出现的
filter + Set 记录 key要保留首次出现需要自己多写几行
reduce 累积去重同时要做变换初始值别忘传 []
JSON.stringify 作 key整个对象内容去重字段顺序影响 key;undefined/函数字段会丢

「去不掉」排查清单

写法没选错还是去不掉?按这个顺序查。

  1. 打印 typeof arr[0]:如果多出来 'object',说明混进了对象,Set 只对原始值有效。
  2. 对两个「重复」元素跑 Object.is(a, b):结果为 false 就不是同一个引用,Set 判定不重复。
  3. 检查 key 选得对不对:用 name 去重但两条记录 name 相同 id 不同,你做的是「按 name 去重」而不是「按 id」。
  4. 后端给的嵌套数据,确认唯一标识在顶层还是嵌套层:item[key] 取错层拿到的是对象而不是原始值。

常见翻车现场

下面这些场景几乎每个项目都会遇到一次。

  • 接口分页合并后 id 重复:先 [...old, ...new] 再按 id 去重;想保留旧数据还是新数据,决定用 keep-first 还是 Map 覆盖。
  • 表单多选标签去重:字符串数组直接 Set 即可,但用户输入常有首尾空格,先 map(s => s.trim()) 再去重。
  • 性能:10 万条数据用双重循环 splice 的老写法会卡死页面,Set/Map 都是 O(n),换掉立竿见影。

常见问题

JS 数组去重最快的方法是什么?

原始值数组用 [...new Set(arr)],一行且 O(n)。对象数组 Set 去不掉,要改用 Map 按 key 去重,两种场景不要混用。

为什么 Set 去不掉对象数组里的重复项?

Set 按引用比较,两个内容相同的对象字面量也是不同引用,都会被保留。要么按唯一 key 用 Map,要么把对象 stringify 后再比较。

按 id 去重想保留第一次出现的对象怎么办?

new Map 会用后出现的覆盖先出现的。改用 filter 加 Set 记录见过的 key,遇到重复直接跳过,即可保留首次出现的元素。

数组里有 NaN 会影响去重吗?

用 Set 不会,它基于 SameValueZero,NaN 也能正确去重;但 filter 加 indexOf 的老写法因 NaN !== NaN 会把 NaN 全部留下。

📂

松开以自动格式化

支持 .json / .sql / .csv / .html / .css / .xml / .yaml 文件