先记住这个答案
Set 用 SameValueZero 判断成员是否重复:NaN 与 NaN 相同,正零和负零相同,其他情况与严格相等的主要行为一致,不做类型转换,对象按身份比较。因此一和字符串一可同时存在,两个结构相同的新对象也不会自动合并。现代规范还会把加入集合的负零归一化为正零,不能说遍历时保留先插入的负号。数组可以通过 [...new Set(values)] 去重,但若需要按用户编号、结构内容或业务状态合并,应先设计明确键和冲突策略。
- NaN 合并,正负零合并且规范化为正零
- 数值与字符串不会被隐式转换后去重
- 对象相同内容不等于相同身份
用特殊数值和对象身份验证规则
下面同时加入两次 NaN、正负零、数值一与字符串一,以及一个对象的重复引用和结构相同的新对象。输出数量可以检验每一组到底被合并还是保留,避免只背算法名称。
负零另做一次身份判断:即使只加入负零,迭代出来的值也按现代规范归一化为正零。这个细节与“重复值不改变插入顺序”是两条规则,不能用后者推导保留零的原始符号。
const item = { id: 7 };
const unique = [...new Set([NaN, NaN, -0, +0, 1, '1', item, item, { id: 7 }])];
console.log(unique.length);
console.log(new Set([1, '1']).size);
console.log(new Set([item, { id: 7 }]).size);
const zero = [...new Set([-0])][0];
console.log(Object.is(zero, -0), Object.is(zero, +0));查看输出与解释
6
2
2
false true总共六类成员;字符串一与数值一分开,两个独立对象也分开。最后两个布尔值证明负零已经归一化,不能依靠 Set 的遍历结果保留输入零的方向信息。
数组去重会丢掉重复次数的信息
标签列表若只关心不同标签,直接 Set 去重再展开通常符合需求,而且保留首次成功插入的成员顺序。若输入里重复次数代表点击量、错误频率或库存数量,去重就会删除业务需要的信息。
对象列表按编号合并时,可以用 Map 以经过校验的业务编号为键,但还必须决定重复编号保留第一条、最后一条还是报冲突。把对象 JSON.stringify 后当键也有表示限制,不能当作通用结构相等算法。
序列化与归一化要在边界上说明
JSON 往返会改变某些值的表示,例如数组中的 NaN 会变成 null,不能期待反序列化后仍保有相同的判重含义。需要跨网络保留特殊值时,应定义显式编码与解码规则。
如果业务确实区分正零和负零,必须在加入集合前保留单独标记或使用其他键表示。性能方面规范要求平均访问时间优于线性,并未强制所有实现都是哈希表或保证每次操作恒定时间。
容易答错的地方
- 认为先加入负零就会保留负号
- 现代集合键处理会归一化负零,因此遍历结果不能保留这个差别。需要符号信息时应在入集合前编码或单独记录,不能事后从已经合并的成员里恢复原始输入。
- 认为对象去重自动比较全部字段
- Set 比较对象身份,两个独立创建的相同结构对象仍是两个成员。按业务字段去重应明确字段合法性、键冲突和保留策略,避免把身份判重误用成数据合并规则。
面试官还会怎么问?
Set 可以区分一和字符串一吗?
可以,SameValueZero 不执行隐式类型转换,类型不同的这两个值是不同成员。若业务希望把它们归为同一编号,应先按受控规则规范化输入,并处理无效字符串等边界。
同一对象改了字段后还是同一成员吗?
是,对象身份没有改变,Set 仍能用这个引用找到它。若业务判重依据可变字段,Set 无法自动重建这套关系;应选取稳定键或明确更新索引,避免字段变化后查找语义失真。
Set 去重后的顺序能直接当排序吗?
它保留成员首次成功加入的顺序,既不是按数值也不是按字典排序。需要排序应显式选择比较规则,再决定先去重还是先排序,以及重复记录应该保留哪份业务信息。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。