map默认按对象引用而非内容去重,因使用samevaluezero算法;需手动将对象序列化为稳定字符串作键,如json.stringify或专用库,并注意undefined、函数、属性顺序等陷阱。

JavaScript 的 Map 默认**不会**根据对象内容做去重,而是基于对象的引用(identity)判断是否相等。也就是说,两个结构完全一样的自定义对象,只要不是同一个引用,Map 就会把它们当作不同的键——这和你期望的“按内容哈希去重”是两回事。
为什么 Map 不自动支持对象内容去重?
Map 的键比较使用的是 SameValueZero 算法(类似 ===,但 0 和 -0 视为相等,NaN 与自身相等)。它不调用 toString()、不序列化、不递归比对属性,只看是不是同一个内存地址。
例如:
const map = new Map();
const a = { id: 1, name: 'Alice' };
const b = { id: 1, name: 'Alice' };
map.set(a, 'valueA');
map.set(b, 'valueB');
console.log(map.size); // 2 —— a 和 b 被视为不同键
console.log(map.get(a)); // 'valueA'
console.log(map.get(b)); // 'valueB'(不是覆盖,是新增)
手动实现“内容去重”的常用方法
核心思路:把对象转成**唯一、稳定、可复现的字符串表示**(即“哈希键”),再用这个字符串作 Map 的真正键。关键在于:同一对象内容 → 同一字符串;不同内容 → 不同字符串(尽量避免碰撞)。
-
简单场景(结构扁平、无函数/循环引用):用
JSON.stringify序列化 -
可靠生产方案:使用标准化序列化库(如
fast-stable-stringify或safe-stable-stringify),它们能处理undefined、Date、Set、Map、循环引用,并保证属性顺序一致 -
高性能或可控场景:为对象定义
.toKey()方法,显式返回业务语义上的唯一标识(比如{ id: 5 }.toKey() → "user:5")
封装一个支持内容去重的 Map 类(推荐)
你可以封装一个轻量工具类,内部用字符串哈希键代理原生 Map:
class ContentMap {
constructor(hashFn = obj => JSON.stringify(obj)) {
this._map = new Map();
this._hash = hashFn;
}
set(obj, value) {
const key = this._hash(obj);
return this._map.set(key, { value, original: obj });
}
get(obj) {
const key = this._hash(obj);
const entry = this._map.get(key);
return entry?.value;
}
has(obj) {
return this._map.has(this._hash(obj));
}
delete(obj) {
return this._map.delete(this._hash(obj));
}
}
// 使用示例
const cmap = new ContentMap(obj => JSON.stringify(obj));
cmap.set({ id: 1 }, 'user1');
cmap.set({ id: 1 }, 'user1-new'); // 覆盖
console.log(cmap.get({ id: 1 })); // 'user1-new'
注意事项和陷阱
-
JSON.stringify会忽略undefined、函数、Symbol 属性、原型链上的属性,且属性顺序不保证(V8 8.0+ 已稳定,但其他引擎或旧版本可能不一致) - 如果对象含
Date、RegExp、BigInt,默认JSON.stringify会丢失或报错,需自定义 replacer - 不要在哈希函数中使用
Math.random()或时间戳,否则无法复现键值,导致get失败 - 若对象很大,每次
set/get都序列化开销高,可考虑缓存哈希结果(如挂到对象自身obj[Symbol.for('key')] = ...),但要注意内存泄漏风险
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











