键控集合(Keyed Collections)

键控集合是按“键”来存取数据的集合,典型代表是 Map/WeakMap 与 Set/WeakSet。它们都按插入顺序可迭代,并使用 SameValueZero 作为相等性判断(NaN 等于自身,0 与 -0 视为相等)。

参考:Map/Set 的指南与对比来自 MDN Keyed collections(见文末参考)。


Map

Map 是键值对集合(key → value),键可以是任意类型(对象、原始值、非注册 Symbol 都可)。

基本用法

const sayings = new Map();
sayings.set("dog", "woof");
sayings.set("cat", "meow");
sayings.set("elephant", "toot");
console.log(sayings.size); // 3
console.log(sayings.get("dog")); // "woof"
console.log(sayings.get("fox")); // undefined
console.log(sayings.has("bird")); // false
sayings.delete("dog");
console.log(sayings.has("dog")); // false
for (const [key, value] of sayings) {
console.log(`${key} goes ${value}`);
}
// "cat goes meow"
// "elephant goes toot"
sayings.clear();
console.log(sayings.size); // 0

常用迭代接口:

map.keys(); // 可迭代的键
map.values(); // 可迭代的值
map.entries(); // 可迭代的 [key, value]
map.forEach((value, key) => {
/* ... */
});

与 Object 的对比

  • 键类型:Object 的键只能是字符串或 Symbol;Map 的键可为任意值。
  • 大小:Map.size 即可得元素个数;Object 需手动统计。
  • 迭代顺序:Map 按插入顺序;Object 的属性顺序有规则但不用于通用映射。
  • 原型污染:Object 有原型链;可用 Object.create(null) 规避。

选择建议:

  • 运行时才知道键,且键/值类型一致 → 用 Map。
  • 需要非字符串键(如对象、NaN)→ 用 Map。
  • 要频繁增删查、并保持插入顺序迭代 → 用 Map。
  • 有对象语义、需要方法逻辑挂载在“元素”上 → 用 Object。

WeakMap

WeakMap 的键必须是对象或“非注册 Symbol”,且对键是“弱引用”(不会阻止垃圾回收)。它不可枚举(无 keys/values/entries/forEach),也没有 size、clear。

const privates = new WeakMap();
function Public() {
const secret = { token: crypto.randomUUID() };
privates.set(this, secret);
}
Public.prototype.getToken = function () {
return privates.get(this).token; // 访问“私有”数据
};

用途:

  • 为对象附加私有元数据(对象回收后私有数据随之可回收)。
  • 对象缓存/映射而不影响其生命周期。

注意:类的 #private 字段已能覆盖部分私有场景,但 WeakMap 仍适合跨实例、弱引用缓存等。


Set

Set 是唯一值集合(无重复)。按插入顺序可迭代。

基本用法

const mySet = new Set();
mySet.add(1);
mySet.add("some text");
mySet.add("foo");
console.log(mySet.has(1)); // true
mySet.delete("foo");
console.log(mySet.size); // 2
for (const item of mySet) {
console.log(item);
}
// 1
// "some text"

与 Array 的互转与去重

// Array → Set(自动去重)
const s = new Set([1, 2, 2, 3]); // {1,2,3}
// Set → Array
const arr1 = Array.from(s);
const arr2 = [...s];
// 快速去重
const unique = [...new Set([1, 1, 2, 3, 3])]; // [1,2,3]

集合运算(并/交/差)

const A = new Set([1, 2, 3]);
const B = new Set([3, 4]);
// 并集
const union = new Set([...A, ...B]); // {1,2,3,4}
// 交集
const intersection = new Set([...A].filter((x) => B.has(x))); // {3}
// 差集(A-B)
const difference = new Set([...A].filter((x) => !B.has(x))); // {1,2}

与 Array 的对比

  • 删除元素:Set.delete(value) O(1);数组需 indexOf + splice,通常更慢。
  • 包含 NaN:数组 indexOf(NaN) 无法命中;Set.has(NaN) 可命中。
  • 保证唯一性:Set 天生去重;数组需手动去重。

WeakSet

仅存放对象的集合,元素为弱引用;不可枚举,无 size。常用于:

  • 标记对象是否“已处理/已访问”。
  • 不影响对象的垃圾回收。
const visited = new WeakSet();
function traverse(node) {
if (visited.has(node)) return; // 避免重复处理
visited.add(node);
// ... 处理 node 的子节点
}

键和值的相等性(SameValueZero)

Map 与 Set 使用 SameValueZero 判断相等:

  • NaN 等于自身(可作为键或集合成员正常命中)。
  • 0 与 -0 被视为相等。
const m = new Map();
m.set(NaN, "value");
console.log(m.get(NaN)); // "value"
const s = new Set([0]);
console.log(s.has(-0)); // true

实用场景与建议

  • 缓存与记忆化:用 Map/WeakMap 将输入对象映射到计算结果。
  • 元数据附加:在不污染对象结构的情况下,给对象附加信息(WeakMap)。
  • 唯一集合:用 Set 去重、做集合运算;避免用数组手动去重。
  • 生命周期友好:需要对象可被回收时,不要用强引用的 Map/Set 保存其“存在性”;选 WeakMap/WeakSet。
  • 遍历:需要稳定插入顺序遍历时,优先 Map/Set + for...of。

参考

  • MDN:Keyed collections(Map / Set / WeakMap / WeakSet)
    • 链接:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Keyed_collections