如何为TypeScript类定义哈希函数以用作字典/Map的键
TypeScript中将类对象作为字典键的解决方案
问题根本原因
JavaScript/TypeScript 内置的Map默认使用*严格相等(===)*规则判断键是否相同:对于引用类型的对象,严格相等判断的是内存地址是否一致,而非属性值是否相等,因此两个属性完全相同的Point实例会被识别为不同的键。
惯用解决方案
下面是生产环境常用的三种实现方式,可根据业务场景选择:
方案1:自定义实例序列化方法 + 封装专用Map
这是适用范围最广的方案,不需要修改实例创建逻辑,性能和可靠性都比较高。
首先给类添加序列化方法,生成可以唯一标识实例属性的字符串:
class Point { constructor(public x: number, public y: number) {} // 生成唯一标识实例的字符串,JSON序列化可以避免属性值包含分隔符导致的冲突 get uniqueKey(): string { return JSON.stringify({ type: 'Point', x: this.x, y: this.y }); } }
然后封装适配Point类型的Map类,对外暴露和原生Map完全一致的使用接口:
class PointMap<V> implements Map<Point, V> { private innerMap = new Map<string, V>(); set(key: Point, value: V): this { this.innerMap.set(key.uniqueKey, value); return this; } get(key: Point): V | undefined { return this.innerMap.get(key.uniqueKey); } has(key: Point): boolean { return this.innerMap.has(key.uniqueKey); } delete(key: Point): boolean { return this.innerMap.delete(key.uniqueKey); } clear(): void { this.innerMap.clear(); } get size(): number { return this.innerMap.size; } // 剩余Map接口方法可按需实现,这里省略其余遍历相关方法的封装 }
使用效果如下:
const map = new PointMap<number>(); map.set(new Point(0, 1), 1); map.set(new Point(0, 1), 2); console.log(map.size); // 输出 1,符合预期 console.log(map.get(new Point(0, 1))); // 输出 2
方案2:享元模式复用实例
如果相同属性的实例重复率很高,可以用享元模式保证同属性永远只有一个实例,直接使用原生Map即可:
class Point { // 全局实例缓存 private static instanceCache = new Map<string, Point>(); // 构造函数私有化,禁止外部直接new private constructor(public x: number, public y: number) {} // 统一使用静态工厂方法获取实例 static create(x: number, y: number): Point { const cacheKey = `${x},${y}`; if (!Point.instanceCache.has(cacheKey)) { Point.instanceCache.set(cacheKey, new Point(x, y)); } return Point.instanceCache.get(cacheKey)!; } }
使用时不要直接new Point,统一调用Point.create生成实例:
const map = new Map<Point, number>(); map.set(Point.create(0, 1), 1); map.set(Point.create(0, 1), 2); console.log(map.size); // 输出 1,符合预期
该方案的优势是无需修改Map的使用逻辑,缺点是会长期缓存所有创建过的实例,适合实例数量可控、重复率高的场景。
方案3:直接使用序列化字符串作为键
如果只是临时使用,不想额外封装类,可以直接把序列化后的字符串作为原生Map的键,是最轻便的实现:
const map = new Map<string, number>(); const p1 = new Point(0, 1); // 存入时取实例的序列化值作为键 map.set(JSON.stringify({x: p1.x, y: p1.y}), 1); const p2 = new Point(0, 1); // 取值时同样用当前实例的序列化值查找 console.log(map.get(JSON.stringify({x: p2.x, y: p2.y}))); // 输出 1
注意事项
序列化时要避免不同属性生成相同的字符串:如果类属性包含字符串、对象等复杂类型,优先使用JSON.stringify序列化完整的属性结构,不要简单用逗号拼接属性值,避免属性值包含分隔符导致的键冲突。
内容的提问来源于stack exchange,提问作者Yuchen
相关产品推荐
相关产品推荐

