
javascript 的 set 默认基于引用相等性(`===`)判断对象是否存在,无法直接通过 `has()` 检查具有相同属性结构但不同引用的对象。本文详解其原理,并提供基于序列化、自定义 map 封装及实用工具函数的三种可靠解决方案。
在 JavaScript 中,Set 的 add() 和 has() 方法对基本类型(如数字、字符串)能正确识别重复值,但对对象却仅依赖内存引用一致性,而非内容一致性。例如:
const mySet = new Set();
const obj1 = { a: 'b' };
mySet.add(obj1);
console.log(mySet.has({ a: 'b' })); // false —— 新建对象,引用不同
console.log(mySet.has(obj1)); // true —— 同一引用这是因为 Set 内部使用严格相等(===)比较,而 {a: 'b'} === {a: 'b'} 永远为 false —— 它们是两个独立分配在堆内存中的对象实例。
✅ 解决方案一:基于 JSON 序列化的简易匹配(适用于简单可序列化对象)
若对象不含函数、undefined、Symbol、Date、RegExp 或循环引用,可将对象标准化为字符串键进行查找:
class ValueSet {
constructor() {
this._map = new Map();
this._serializer = (obj) => JSON.stringify(obj);
}
add(obj) {
const key = this._serializer(obj);
this._map.set(key, obj);
return this;
}
has(obj) {
return this._map.has(this._serializer(obj));
}
get(obj) {
return this._map.get(this._serializer(obj));
}
values() {
return [...this._map.values()];
}
}
// 使用示例
const valueSet = new ValueSet();
valueSet.add({ a: 'b' });
console.log(valueSet.has({ a: 'b' })); // true
console.log(valueSet.has({ b: 'a' })); // false⚠️ 注意:JSON.stringify() 会忽略 undefined、函数和 Symbol 属性,且对键顺序敏感({a:1,b:2} ≠ {b:2,a:1} 在某些旧环境)。如需稳定排序,可先对键名排序再序列化。
立即学习“Java免费学习笔记(深入)”;
✅ 解决方案二:通用深比较 + Map 封装(推荐用于生产环境)
借助轻量深比较库(如 fast-deep-equal)或手写简化版,构建支持语义去重的集合:
// 简化版浅层深比较(仅支持扁平对象/数组)
function deepEqual(a, b) {
if (a === b) return true;
if (a == null || b == null || typeof a !== 'object' || typeof b !== 'object') return false;
const keysA = Object.keys(a), keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;
}
return true;
}
class DeepSet {
constructor() {
this._items = [];
}
add(obj) {
if (!this._items.some(item => deepEqual(item, obj))) {
this._items.push(obj);
}
return this;
}
has(obj) {
return this._items.some(item => deepEqual(item, obj));
}
delete(obj) {
const index = this._items.findIndex(item => deepEqual(item, obj));
if (index > -1) {
this._items.splice(index, 1);
return true;
}
return false;
}
size() {
return this._items.length;
}
values() {
return [...this._items];
}
}
// 使用
const deepSet = new DeepSet();
deepSet.add({ name: 'Alice', age: 30 });
console.log(deepSet.has({ name: 'Alice', age: 30 })); // true
console.log(deepSet.has({ name: 'Alice', age: 31 })); // false✅ 优势:不依赖序列化,支持 undefined、null、数组嵌套;
❌ 缺点:O(n) 查找复杂度,大数据量时性能低于原生 Set(O(1))。
✅ 解决方案三:业务场景定制唯一键(最佳实践)
最高效的方式是避免依赖对象内容比较,而是为每个对象定义稳定的唯一标识(ID),用 Map 替代 Set:
class EntitySet {
constructor(idKey = 'id') {
this._map = new Map();
this._idKey = idKey;
}
_getId(obj) {
return obj[this._idKey] ?? Symbol('no-id');
}
add(obj) {
const id = this._getId(obj);
this._map.set(id, obj);
return this;
}
has(obj) {
return this._map.has(this._getId(obj));
}
get(obj) {
return this._map.get(this._getId(obj));
}
}
// 假设所有对象都有 id 字段
const userSet = new EntitySet('id');
userSet.add({ id: 'usr-123', name: 'Bob' });
console.log(userSet.has({ id: 'usr-123' })); // true? 总结建议:
- 优先采用 ID 映射方案(方案三) —— 高效、可控、符合工程规范;
- 若必须按内容判重且对象结构简单,选用 JSON 序列化方案(方案一);
- 对复杂嵌套结构且需强语义匹配,选用 深比较封装(方案二),并注意性能边界;
- 切勿在大型集合中频繁调用 has() 于未索引的深比较实现 —— 考虑引入缓存或预计算哈希值优化。










