B07 Map 与 Set 集合类型
B07 Map 与 Set 集合类型
摘要:对象的键只能是字符串与 Symbol,且会继承原型链上的属性;
Map允许任意值作为键并保持插入顺序,Set保证值唯一。WeakMap与WeakSet的键是弱引用,不阻止回收,适合给对象挂载私有数据。本文给出四类集合的增删改查、迭代顺序、与对象的取舍对照,以及WeakMap缓存与私有状态两个典型用法。
关键词:Map,Set,WeakMap,弱引用,迭代顺序
所属:frontend/js / B-进阶 | 上一篇:B06-错误处理与调试 | 下一篇:B08-正则表达式 | 返回:B00-总结
1 引言
用对象当字典时会遇到三类问题:键 1 与 "1" 冲突、遍历顺序不保证、键名与原型属性(toString、constructor)可能撞车。用对象数组做去重则要写嵌套循环。
本文给出四类集合类型的选择依据。范围见表 1-1。
| 项 | 本文覆盖 | 本文不覆盖 |
|---|---|---|
| 键值 | Map 增删改查、迭代顺序、链式 |
自定义哈希结构 |
| 唯一值 | Set 去重、集合运算 |
不可变集合库 |
| 弱引用 | WeakMap/WeakSet 用途与限制 |
垃圾回收算法(C03) |
| 取舍 | 四类集合与对象的对照 | JSON 序列化(C06) |
表 1-1 本文覆盖范围
2 预备知识
| 项 | 要求 | 核对方式 |
|---|---|---|
| 环境 | Node.js 20+ 或 Chrome 130+ | 能执行 ESM 脚本 |
| 前置 | A06、C03 | 数组与可达性已掌握 |
| 文件 | js-practice/collections2.mjs |
编码 UTF-8 |
表 2-1 环境与前置要求
3 原理与实现
3.1 四类集合的边界
按「键的类型」与「是否弱引用」划分,如图 3-1 所示。
flowchart TD
A["需要键值对"] --> B{"键是否必须为对象"}
B -->|是| C{"是否需要遍历"}
C -->|否| D["WeakMap<br/>键弱引用,不可遍历"]
C -->|是| E["Map 以对象为键"]
B -->|否| F["Map"]
G["只需要值的唯一性"] --> H{"值是否为对象且不需遍历"}
H -->|是| I["WeakSet"]
H -->|否| J["Set"]
图 3-1 四类集合按键类型与遍历需求划分
图上可见:弱引用集合不可遍历、没有
size,这是回收器能随时回收键的代价;需要遍历就必须用Map/Set。
3.2 完整实现
// js-practice/collections2.mjs;运行环境:Node.js 20+
// 1 Map:任意键,保持插入顺序
const counts = new Map();
const keyObj = { id: 1 };
counts.set('a', 1);
counts.set(2, 'two');
counts.set(keyObj, '对象键');
console.log(counts.get(keyObj)); // 对象键
console.log(counts.get('2')); // undefined,2 与 '2' 是不同键
console.log([...counts.keys()]); // ['a', 2, { id: 1 }]
console.log(counts.size, counts.has('a')); // 3 true
// 2 Map 与对象的取舍:对象做字典会撞原型属性
const dict = Object.create(null); // 无原型,避免 toString 冲突
dict.toString = 'ok';
console.log(dict.toString); // ok
// 3 Set:去重与集合运算
const ids = [3, 1, 2, 1, 3];
const unique = [...new Set(ids)];
console.log(unique); // [3, 1, 2]
const online = new Set([1, 2, 3]);
const banned = new Set([3]);
const active = [...online].filter((id) => !banned.has(id)); // 差集
console.log(active); // [1, 2]
console.log(online.difference?.(banned) ?? '当前运行时不支持 difference');
// 4 WeakMap:给对象挂私有数据,键被回收时条目自动消失
const privateData = new WeakMap();
class User {
constructor(name) {
privateData.set(this, { name, loginAt: Date.now() });
}
get name() {
return privateData.get(this).name;
}
}
const u = new User('Lin');
console.log(u.name, Object.keys(u)); // Lin [],外部看不到内部数据
// 5 WeakMap 做对象级缓存
const cache = new WeakMap();
function heavy(obj) {
if (cache.has(obj)) return cache.get(obj);
const result = Object.keys(obj).length;
cache.set(obj, result);
return result;
}
const payload = { a: 1, b: 2 };
console.log(heavy(payload), heavy(payload)); // 2 2
console.log(cache.size); // undefined,弱引用集合无 size
四类集合与对象的对照见表 3-1。
| 需求 | 首选 | 原因 |
|---|---|---|
| 键为字符串,结构固定 | 对象 | 字面量简洁,可直接解构 |
| 键为任意值或需要保序 | Map |
键不转字符串,插入顺序稳定 |
| 需要频繁增删键值对 | Map |
有 size,遍历无需 Object.keys |
| 值去重 | Set |
has 为常数时间,数组为线性 |
| 给对象挂数据且不应泄漏 | WeakMap |
键被回收时条目自动移除 |
| 标记对象是否已处理 | WeakSet |
不阻止被标记对象被回收 |
表 3-1 集合类型的选择依据
常用方法见表 3-2。
| 操作 | Map |
Set |
WeakMap |
|---|---|---|---|
| 增 | set(key, value) 可链式 |
add(value) |
set(key, value) |
| 查 | get(key) |
has(value) |
get(key) |
| 删 | delete(key) |
delete(value) |
delete(key) |
| 清空 | clear() |
clear() |
无 |
| 数量 | size |
size |
无 |
| 遍历 | keys/values/entries |
values/entries |
不可遍历 |
| 可序列化 | 需先转数组(C06) | 需先转数组 | 否 |
表 3-2 三类集合的方法对照
4 验证与讨论
- 执行脚本,确认
counts.get('2')为undefined,说明Map的键不转字符串。 - 把
dict改为{}后再读取dict.toString,确认拿到原型上的函数而非字符串。 - 在控制台执行
[...new Set([3,1,2,1,3])],确认顺序保留首次出现次序。 - 把
privateData改为Map,确认实例被丢弃后条目仍存在,堆快照中可见滞留。
| 检查项 | 预期结果 | 说明 |
|---|---|---|
| 键类型 | 对象键与字符串键共存 | 表 3-1 |
| 插入顺序 | 遍历顺序与写入一致 | 3.2 节 |
| 去重 | 重复值只保留一个 | Set |
| 私有性 | Object.keys(u) 为空 |
WeakMap 不在实例上留属性 |
| 无 size | cache.size 为 undefined |
弱引用集合不可统计 |
表 4-1 五项验收
讨论与局限。 Set 的唯一性基于 SameValueZero 比较,NaN 等于自身,+0 与 -0 视为同一个值,对象按引用比较。另,difference、union 等集合运算方法在较旧运行时不可用,需手写 filter 实现。
5 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
map.get('2') 取不到值 |
写入时用了数字 2 |
统一键的类型 |
| 用对象当字典时多了属性 | 原型链上的同名属性 | Object.create(null) 或改用 Map |
JSON.stringify(map) 得到 {} |
Map 不可直接序列化 | 先 [...map] 转数组,见 C06 |
| 缓存导致内存不降 | 用 Map 做对象级缓存 |
改用 WeakMap |
遍历 WeakMap 报错 |
弱引用集合不支持遍历 | 需要遍历时改用 Map |
| 去重后顺序变了 | 用了排序或多轮 filter |
Set 保留首次出现顺序 |
表 5-1 集合类型的六类问题
6 结论
| 序号 | 结论 | 依据 |
|---|---|---|
| 1 | 键为任意值或需保序时用 Map |
表 3-1 |
| 2 | 值去重与存在性判断用 Set |
表 3-1 |
| 3 | 给对象挂私有数据用 WeakMap |
图 3-1 |
| 4 | 弱引用集合不可遍历、无 size |
表 3-2 |
表 6-1 结论汇总
- 上一篇:B06-错误处理与调试
- 下一篇:B08-正则表达式
- 返回:B00-总结 | 科目 README
参考文献
[1] MDN Web Docs · Map,https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map(访问于 2026-10-31)
[2] ECMA-262 · 24.3 WeakMap Objects,https://tc39.es/ecma262/#sec-weakmap-objects(访问于 2026-10-31)

评论 0
还没有评论,快来抢沙发吧~