Map、Set 与弱引用集合
普通对象只能使用字符串作为键,这在很多场景下不够灵活。ES6 引入了 Map 与 Set 两套全新的集合类型,之后又补充了弱引用版本 WeakMap、WeakSet,以及更底层的 WeakRef 与 FinalizationRegistry。本文从基本用法讲起,逐步深入到弱引用机制的实际应用。
一、Map 的创建与常用方法
Map 保存键值对,键可以是任意类型的值(包括对象、函数、NaN),且记住插入顺序。
1.1 创建 Map
javascript
// 空 Map
const m1 = new Map();
// 从二维数组(键值对数组)创建
const m2 = new Map([
["name", "张三"],
["age", 25],
]);
// 从另一个 Map 创建
const m3 = new Map(m2);
console.log(m2.size); // 21.2 增删改查
| 方法 | 作用 | 示例 | 返回值 |
|---|---|---|---|
set(key, value) | 新增或更新键值 | m.set("a", 1) | Map 本身(可链式调用) |
get(key) | 读取值 | m.get("a") | 值或 undefined |
has(key) | 是否存在 | m.has("a") | 布尔值 |
delete(key) | 删除键 | m.delete("a") | 布尔值 |
clear() | 清空所有 | m.clear() | undefined |
size | 键值对数量 | m.size | 数字 |
javascript
const m = new Map();
m.set("name", "张三").set("age", 25); // set 返回自身,支持链式调用
console.log(m.get("name")); // 张三
console.log(m.has("age")); // true
console.log(m.size); // 2
// 键为对象时按引用比较
const key = {};
m.set(key, "对象键");
console.log(m.get(key)); // 对象键
console.log(m.get({})); // undefined,字面量 {} 是另一个对象
m.delete("age");
console.log(m.has("age")); // false
m.clear();
console.log(m.size); // 0注意:set 以"值是否严格相等"来判断键是否已存在,因此 NaN 也可以作为键(NaN !== NaN,但 Map 内部将其视为同一个键):
javascript
const m = new Map();
m.set(NaN, 1);
m.set(NaN, 2); // 覆盖
console.log(m.get(NaN)); // 21.3 迭代 Map
Map 是可迭代对象,有 4 种遍历方式:
javascript
const m = new Map([
["name", "张三"],
["age", 25],
]);
// keys():遍历键
for (const k of m.keys()) console.log(k); // name age
// values():遍历值
for (const v of m.values()) console.log(v); // 张三 25
// entries():遍历键值对
for (const [k, v] of m.entries()) console.log(k, v);
// 直接遍历 Map 等价于 entries()
for (const [k, v] of m) console.log(k, v);
// forEach
m.forEach((value, key) => console.log(key, value));
// 转成数组
console.log([...m]); // [['name','张三'], ['age',25]]
console.log(Array.from(m)); // 同上1.4 Map 合并
javascript
const a = new Map([["x", 1]]);
const b = new Map([["y", 2]]);
const c = new Map([...a, ...b, ["z", 3]]);
console.log([...c]); // [['x',1], ['y',2], ['z',3]]二、Map 与 Object 对比
| 对比项 | Object | Map |
|---|---|---|
| 键的类型 | 只能是字符串或 Symbol(数字、布尔会被转为字符串) | 任意类型,按引用精确比较 |
| 键的顺序 | 整数键按升序,其他按插入序(有例外) | 严格按插入顺序 |
size 属性 | 无,需 Object.keys(obj).length | 直接 map.size |
| 性能 | 频繁增删键时较慢 | 频繁增删键更快 |
| 迭代 | for...in 需额外判断 hasOwnProperty | 原生可迭代,直接 for...of |
| 原型 | 有原型链,键可能被原型污染 | 无原型污染问题 |
| 序列化 | JSON.stringify 原生支持 | 需手动转换 |
javascript
// Object 中键会被转成字符串
const obj = {};
obj[1] = "数字";
obj["1"] = "字符串";
console.log(obj); // { 1: '字符串' },两者是同一个键
// Map 中数字 1 与字符串 "1" 是两个键
const m = new Map();
m.set(1, "数字");
m.set("1", "字符串");
console.log(m.size); // 2性能对比示例:频繁写入时 Map 更合适。
javascript
const n = 1_000_000;
const obj = {};
const map = new Map();
console.time("obj 写入");
for (let i = 0; i < n; i++) obj["k" + i] = i;
console.timeEnd("obj 写入");
console.time("map 写入");
for (let i = 0; i < n; i++) map.set("k" + i, i);
console.timeEnd("map 写入");三、Map 的实际应用
3.1 缓存(memoize)
用 Map 保存函数调用结果,对象作为键时天然适合:
javascript
function memoize(fn) {
const cache = new Map();
return function (arg) {
if (cache.has(arg)) {
console.log("命中缓存");
return cache.get(arg);
}
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
const expensive = memoize((n) => {
console.log("计算中...");
return n * n;
});
console.log(expensive(4)); // 计算中... 16
console.log(expensive(4)); // 命中缓存 163.2 频次统计
javascript
const words = ["苹果", "香蕉", "苹果", "橙子", "香蕉", "苹果"];
const count = new Map();
for (const w of words) {
count.set(w, (count.get(w) || 0) + 1);
}
console.log([...count]); // [['苹果',3], ['香蕉',2], ['橙子',1]]3.3 用对象作键关联数据
javascript
const user1 = { id: 1 };
const user2 = { id: 2 };
const onlineMap = new Map();
onlineMap.set(user1, { lastLogin: "2026-08-01" });
onlineMap.set(user2, { lastLogin: "2026-08-05" });
console.log(onlineMap.get(user1).lastLogin); // 2026-08-01四、Set 的创建与方法
Set 保存唯一值的集合,同样记住插入顺序,没有键值对的概念。
| 方法/属性 | 作用 | 示例 |
|---|---|---|
add(value) | 添加元素,重复则忽略 | s.add(1) |
has(value) | 是否存在 | s.has(1) |
delete(value) | 删除元素 | s.delete(1) |
clear() | 清空集合 | s.clear() |
size | 元素个数 | s.size |
javascript
const s = new Set();
s.add(1).add(2).add(1); // 重复的 1 被忽略
console.log(s.size); // 2
console.log(s.has(2)); // true
// 从可迭代对象创建
const s2 = new Set([1, 2, 3, 4, 3, 2]);
console.log(s2.size); // 4
// 迭代
for (const v of s2) console.log(v);
s2.forEach((v) => console.log(v));
// Set 的键与值是同一个
console.log(s2.keys()); // SetIterator
console.log([...s2.entries()]); // [[1,1],[2,2],[3,3],[4,4]]五、Set 去重与集合运算
5.1 数组去重
javascript
const arr = [1, 2, 2, 3, 3, 3, "a", "a"];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 'a']
// 字符串去重
console.log([...new Set("abbcccd")].join("")); // abcd5.2 集合运算
javascript
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5]);
// 并集
console.log(new Set([...a, ...b])); // Set {1,2,3,4,5}
// 交集
console.log(new Set([...a].filter((x) => b.has(x)))); // Set {3,4}
// 差集(a 有而 b 没有)
console.log(new Set([...a].filter((x) => !b.has(x)))); // Set {1,2}5.3 判断数组是否包含重复元素
javascript
function hasDuplicate(arr) {
return new Set(arr).size !== arr.length;
}
console.log(hasDuplicate([1, 2, 3])); // false
console.log(hasDuplicate([1, 2, 2])); // true六、Set 与 Array 对比
| 对比项 | Array | Set |
|---|---|---|
| 元素唯一性 | 可以有重复 | 自动去重 |
| 查找效率 | indexOf 为线性查找,慢 | has 更快 |
| 删除元素 | splice 需定位索引 | delete 直接按值删除 |
| 索引访问 | 支持 arr[i] | 不支持 |
| 排序 | 原生 sort | 需转数组再排序 |
| 插入顺序 | 维护 | 维护 |
javascript
const big = 100_000;
const arr = Array.from({ length: big }, (_, i) => i);
const set = new Set(arr);
console.time("array indexOf");
arr.indexOf(big - 1);
console.timeEnd("array indexOf");
console.time("set has");
set.has(big - 1);
console.timeEnd("set has");七、WeakMap
WeakMap 是 Map 的弱引用版本,具有以下特点:
- 键必须是对象(或函数),不能是原始值;
- 弱引用:键对象如果没有其他引用,会被垃圾回收(GC)回收,对应键值对自动消失;
- 不可迭代:没有
keys、values、entries、forEach,也没有size和clear; - 只有
set、get、has、delete四个方法。
javascript
const wm = new WeakMap();
wm.set({}, "无法被访问,因为键没有外部引用");
// 上面的键值对很快会被 GC 回收,无法读取
const key = { id: 1 };
wm.set(key, "可访问");
console.log(wm.get(key)); // 可访问
key = null; // 取消引用后,该键值对也会被回收八、WeakMap 的应用场景
8.1 私有数据
用 WeakMap 保存实例私有字段,外部无法通过对象本身读取:
javascript
const _secret = new WeakMap();
class User {
constructor(name, password) {
_secret.set(this, { password });
this.name = name;
}
checkPassword(pwd) {
return _secret.get(this).password === pwd;
}
}
const u = new User("张三", "123456");
console.log(u.checkPassword("123456")); // true
console.log(u.password); // undefined,外部读不到私有字段
console.log(_secret.get(u)); // { password: '123456' }8.2 为 DOM 节点关联数据
javascript
const nodeData = new WeakMap();
function setNodeData(node, data) {
nodeData.set(node, data);
}
function getNodeData(node) {
return nodeData.get(node);
}
// 节点被移除后,不再被页面引用,其数据也会被自动回收
const div = document.createElement("div");
setNodeData(div, { clickCount: 0 });
console.log(getNodeData(div)); // { clickCount: 0 }8.3 防止内存泄漏
对比普通 Map:只要 Map 还存活,即使业务对象已无用,键也无法回收。而 WeakMap 不会阻止垃圾回收:
javascript
// 错误示例:Map 强引用导致缓存对象无法回收
const cache = new Map();
function process(obj) {
cache.set(obj, new Date());
}
// obj 用完后即使置为 null,只要 cache 还在,对象永远无法回收
// 正确示例:WeakMap 弱引用,对象可被正常回收
const weakCache = new WeakMap();
function process2(obj) {
weakCache.set(obj, new Date());
}九、WeakSet
WeakSet 是 Set 的弱引用版本,特点:
- 元素只能是对象;
- 弱引用,元素无其他引用时会被回收;
- 不可迭代,没有
size、clear,只有add、has、delete。
典型应用:标记对象是否被访问过、防止重复处理。
javascript
const visited = new WeakSet();
function visit(node) {
if (visited.has(node)) {
console.log("已访问过,跳过");
return;
}
visited.add(node);
console.log("首次访问", node.name);
}
visit({ name: "A" }); // 首次访问 A
visit({ name: "A" }); // 首次访问 A(新对象)
const node = { name: "B" };
visit(node); // 首次访问 B
visit(node); // 已访问过,跳过十、WeakRef 与 FinalizationRegistry 简介
10.1 WeakRef
WeakRef 创建对象的弱引用,不阻止对象被回收:
javascript
let obj = { data: "重要数据" };
const ref = new WeakRef(obj);
obj = null; // 原对象只被 ref 弱引用,随时可能被 GC
// deref() 取回对象,若已被回收则返回 undefined
const stillAlive = ref.deref();
console.log(stillAlive ? stillAlive.data : "对象已被回收");注意:deref 返回的对象可能"复活",应尽量把结果保存在局部变量中再使用,避免代码执行期间被回收。
10.2 FinalizationRegistry
FinalizationRegistry 在对象被回收后执行回调,常用于清理外部资源:
javascript
const registry = new FinalizationRegistry((heldValue) => {
console.log("对象已被回收,清理资源:", heldValue);
});
let obj = { name: "临时对象" };
registry.register(obj, "临时对象的清理句柄");
obj = null; // 触发 GC 后(无法手动保证时机),回调会被调用10.3 使用注意
| 工具 | 作用 | 触发时机 | 常见场景 |
|---|---|---|---|
WeakRef | 持有弱引用,可尝试取回 | 取回时机不定 | 大型缓存对象 |
FinalizationRegistry | 对象回收后执行回调 | GC 之后,时机不定 | 释放文件句柄、连接等资源 |
使用规范:
WeakRef与FinalizationRegistry的行为依赖垃圾回收时机,结果不可预测,业务逻辑不应依赖回调是否执行;- 优先使用
WeakMap/WeakSet解决"对象关联数据"问题,只有需要"取回"或"回收后通知"时才考虑WeakRef与FinalizationRegistry; - 回调中不要注册新的引用,也不要依赖被回收对象本身。