WinddSnow

JavaScript-Map-Set-WeakMap-WeakSet

字数统计: 2.8k阅读时长: 11 min
2026/07/30

第60课:Map、Set 与 Weak 版本——集合类型、WeakMap 用途、内存泄漏防护

在 ES6 之前,JavaScript 中管理键值对集合只能依赖普通对象 {}(键只能是字符串或 Symbol),而管理不重复的值集合则需手动通过数组模拟。ES6 引入了四个新的集合类型:**Map(键值对集合,键可以是任意类型)、Set(唯一值集合)、WeakMap(键必须是对象且被弱引用的 Map)和 WeakSet(值必须是对象且被弱引用)。它们各自解决了特定场景下的痛点,尤其是 WeakMap私有数据存储内存泄漏防护**方面有不可替代的作用。本节课将深入每种集合类型的 API、使用场景和性能特性,并对比它们与普通对象的区别。


1. Map:键值对集合,键可以是任意类型

1.1 MapObject 的核心区别

特性 Map Object
键的类型 任意值:对象、函数、基本类型均可。 仅字符串或 Symbol。
键的顺序 严格按插入顺序 字符串键按插入顺序(ES2015+),Symbol 键在最后。
大小获取 map.size 属性直接获取。 Object.keys(obj).length 手动计算。
迭代 直接可迭代:for-offorEachentries() 等。 Object.entries(obj) 转换后迭代。
性能 频繁增删键值对时更优 适合少量固定键,频繁增删时性能不如 Map。
原型链干扰 无原型属性污染,纯数据容器。 继承 Object.prototype,键可能被 toString 等污染。

何时选择 Map:键的类型不限于字符串、需要保持插入顺序、需要频繁增删、需要直接获取大小、需要避免原型污染。

1.2 创建与基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const map = new Map([
['key1', 'value1'],
[42, 'number key'],
[{ id: 1 }, 'object key']
]);

// 添加/更新
map.set('name', 'Alice');
map.set('name', 'Bob'); // 同名键会覆盖

// 获取
console.log(map.get('name')); // 'Bob'
console.log(map.get(42)); // 'number key'

// 检查键是否存在
console.log(map.has('name')); // true

// 删除
map.delete(42);

// 大小
console.log(map.size); // 3

// 清空
map.clear();

1.3 遍历 Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);

// for-of 遍历 [key, value]
for (const [key, value] of map) {
console.log(key, value);
}

// forEach
map.forEach((value, key) => {
console.log(key, value);
});

// 仅键
console.log([...map.keys()]); // ['a', 'b', 'c']

// 仅值
console.log([...map.values()]); // [1, 2, 3]

1.4 Map 键的相等性判断

Map 使用 SameValueZero 算法判断键是否相等。与 === 的主要区别是:NaN 被视为与自身相等(Map 允许 NaN 作为键),且 +0-0 视为相等。

1
2
3
4
5
6
7
const map = new Map();
map.set(NaN, 'value');
console.log(map.get(NaN)); // 'value' —— NaN 被视为同一个键

map.set(0, 'zero');
map.set(-0, 'negative zero');
console.log(map.get(0)); // 'negative zero' —— -0 覆盖了 0

2. Set:唯一值集合

Set 存储不重复的值,值的相等性同样基于 SameValueZero 算法。

2.1 基本操作

1
2
3
4
5
6
7
8
9
10
11
12
const set = new Set([1, 2, 2, 3, 3, 4]);
console.log(set); // Set(4) { 1, 2, 3, 4 } —— 自动去重

set.add(5);
set.add(1); // 重复,被忽略
console.log(set.size); // 5

console.log(set.has(3)); // true
set.delete(2);
console.log(set.has(2)); // false

set.clear();

2.2 数组去重与集合运算

Set 与数组之间的转换非常简便,常用于去重。

1
2
3
const arr = [1, 1, 2, 3, 3, 4];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4]

集合运算(交集、并集、差集)可通过 Set 与数组方法的组合实现:

1
2
3
4
5
6
7
8
9
10
11
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);

// 并集
const union = new Set([...a, ...b]); // {1,2,3,4}

// 交集
const intersection = new Set([...a].filter(x => b.has(x))); // {2,3}

// 差集 (a - b)
const difference = new Set([...a].filter(x => !b.has(x))); // {1}

2.3 遍历 Set

Set 同样支持 for-offorEachkeys()values()keys()values() 行为一致,返回值的迭代器)和 entries()(返回 [value, value] 以保持与 Map 接口一致)。

1
2
3
4
const set = new Set(['apple', 'banana', 'cherry']);
for (const item of set) {
console.log(item);
}

3. WeakMap:键弱引用,自动垃圾回收

WeakMap 是一种特殊的 Map,其键必须是对象,且该引用是弱引用——如果外部没有其他地方引用该键对象,垃圾回收器可以将其回收,对应的键值对也会从 WeakMap 中自动移除。WeakMap 不可迭代,没有 size 属性,没有 clear() 方法,无法遍历键或值。

3.1 基本 API

1
2
3
4
5
6
7
8
9
const weakMap = new WeakMap();
let obj = { id: 1 };
weakMap.set(obj, 'metadata');

console.log(weakMap.get(obj)); // 'metadata'
console.log(weakMap.has(obj)); // true

weakMap.delete(obj);
console.log(weakMap.has(obj)); // false

obj = null 后,该对象仅被 WeakMap 弱引用,垃圾回收器将自动回收该键值对。开发者无法观察这一过程(因为没有迭代器),但这正是 WeakMap 的价值——不会阻止垃圾回收。

3.2 典型用途:存储关联的私有数据

WeakMap 常用于存储与 DOM 元素或对象实例绑定的数据,当 DOM 元素从文档中移除且无其他引用时,相关数据会被自动清理,避免内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 存储每个 DOM 节点的点击次数
const clickCounts = new WeakMap();

document.querySelectorAll('button').forEach(button => {
clickCounts.set(button, 0);
button.addEventListener('click', () => {
const count = clickCounts.get(button) + 1;
clickCounts.set(button, count);
console.log(`点击次数:${count}`);
});
});

// 当 button 从 DOM 中移除且无其他引用时,对应的 clickCounts 条目会被垃圾回收

3.3 实现真正的私有属性(ES6 Class 之前)

在 ES2022 私有字段(#)出现之前,WeakMap 是模拟类实例私有变量的可靠手段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const _private = new WeakMap();

class Person {
constructor(name) {
_private.set(this, { name });
}

getName() {
return _private.get(this).name;
}
}

const p = new Person('Alice');
console.log(p.getName()); // 'Alice'
// 无法从外部直接访问 p.name 或 _private 中的值

4. WeakSet:值弱引用,不可重复

WeakSetSet 的弱引用版本,其值必须是对象,且是弱引用。同样不可迭代,没有 size。主要用于标记对象的状态(如“是否已处理”),当对象被销毁时,标记自动消失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const processedItems = new WeakSet();

function process(obj) {
if (processedItems.has(obj)) {
console.log('已处理过');
return;
}
// 处理逻辑...
processedItems.add(obj);
}

const item = { data: 'some data' };
process(item);
process(item); // 第二次调用会提示“已处理过”

与使用 Set 存储对象相比,WeakSet 不会阻止对象被垃圾回收,适合作为对象生命周期的标记。


5. 对比总结

类型 键/值要求 迭代性 弱引用 典型场景
Map 键任意类型 可迭代 通用键值存储,替代普通对象。
Set 值任意类型 可迭代 去重、集合运算。
WeakMap 键必须是对象 不可迭代 绑定外部对象的元数据,防止内存泄漏。
WeakSet 值必须是对象 不可迭代 标记对象状态(如已访问、已处理)。

选择原则

  • 如果键值对中的键需要使用对象本身,且希望在对象被销毁后自动清理,用 WeakMap
  • 如果需要存储不重复的值列表,用 Set
  • 如果仅需标记对象而不存储额外数据,用 WeakSet
  • 通用键值存储用 Map

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选)Map 中,键的相等性判断使用什么算法?
    A. ===
    B. Object.is
    C. SameValue
    D. SameValueZero

  2. (单选) WeakMap 的键必须是?
    A. 字符串
    B. 对象
    C. Symbol
    D. 任意类型

  3. (填空) Set 对象内部值不重复,可以用来快速实现数组去重:const unique = [______ new Set(arr)]

  4. (多选) 以下哪些操作在 WeakMap 上是不可用的?
    A. get(key)
    B. size
    C. has(key)
    D. forEach

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要编写一个 createCache(limit) 函数,利用 Map 实现一个有容量限制的 LRU(最近最少使用)缓存。要求如下:

  • 缓存容量为 limit,默认值为 10。
  • 提供 get(key) 方法:如果键存在,返回对应的值,并将该键值对移动到“最近使用”的位置;否则返回 undefined
  • 提供 put(key, value) 方法:添加键值对。如果键已存在,更新值并移动到“最近使用”位置;如果键不存在,添加新条目。如果缓存已满(达到 limit),删除最久未使用的条目(即最早插入或最久未访问的条目)。
  • 使用 Map 的键插入顺序特性实现(Map 保证迭代顺序与插入顺序一致,getput 访问时需要将其删除再重新插入以更新顺序)。
  • 使用 class 语法封装,添加 JSDoc 注释说明容量限制和 LRU 算法。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定 Map 的插入顺序特性如何用于 LRU、getput 的实现逻辑。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请使用 JavaScript 实现一个基于 Map 的 LRU 缓存类 LRUCache。需要创建文件 src/lru-cache.js

  • 类构造函数接收 limit 参数(默认 10),内部使用 this.cache = new Map() 存储数据。
  • get(key) 方法:若 this.cache.has(key),获取值 val = this.cache.get(key),将其删除并重新 this.cache.set(key, val)(以更新插入顺序),返回 val;否则返回 undefined
  • put(key, value) 方法:若 this.cache.has(key),删除旧条目;若 this.cache.size >= this.limit,删除最久未使用的条目(即 this.cache.keys().next().value)。然后 this.cache.set(key, value)
  • 为类和所有方法添加 JSDoc 注释。在文件末尾添加一个简短的使用示例,创建容量为 3 的缓存,插入 4 个条目并验证淘汰逻辑。
  • 使用 ESM 语法导出类。确保代码可直接在 Node.js 或现代浏览器环境中运行。

课后练习答案

一、概念自测答案

  1. D

    • 解析:Map 使用 SameValueZero 算法,NaN 与自身相等,+0-0 相等。
  2. B

    • 解析:WeakMap 的键必须是对象(或非 null 的 Object 类型),不能是基本类型。
  3. **...**(展开运算符)

    • 解析:[...new Set(arr)]Array.from(new Set(arr)) 均可实现去重。
  4. B、D

    • 解析:WeakMap 没有 size 属性且不可迭代,因此 forEach 也不可用。gethas 是可用的。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请用 JavaScript 实现一个 LRU 缓存类 LRUCache,利用 Map 的插入顺序保持访问顺序。要求:

  • 构造函数接收 limit(默认 10),内部用 this.cache = new Map()
  • get(key):如果 key 存在,获取值,删除该键并重新 set(更新顺序),返回值;否则返回 undefined
  • put(key, value):如果 key 存在,删除旧条目;如果缓存已满(this.cache.size >= this.limit),使用 this.cache.keys().next().value 获取最久未使用的键并删除。然后 this.cache.set(key, value)
  • 添加 JSDoc 注释,说明 LRU 机制。
  • 提供示例:创建容量为 3 的缓存,依次 put(1,’a’), put(2,’b’), put(3,’c’),然后 get(1),再 put(4,’d’),此时键 2 应被淘汰。
  • 代码整洁,使用 ESM 导出。直接输出完整代码。”
CATALOG
  1. 1. 第60课:Map、Set 与 Weak 版本——集合类型、WeakMap 用途、内存泄漏防护
    1. 1.1. 1. Map:键值对集合,键可以是任意类型
      1. 1.1.1. 1.1 Map 与 Object 的核心区别
      2. 1.1.2. 1.2 创建与基本操作
      3. 1.1.3. 1.3 遍历 Map
      4. 1.1.4. 1.4 Map 键的相等性判断
    2. 1.2. 2. Set:唯一值集合
      1. 1.2.1. 2.1 基本操作
      2. 1.2.2. 2.2 数组去重与集合运算
      3. 1.2.3. 2.3 遍历 Set
    3. 1.3. 3. WeakMap:键弱引用,自动垃圾回收
      1. 1.3.1. 3.1 基本 API
      2. 1.3.2. 3.2 典型用途:存储关联的私有数据
      3. 1.3.3. 3.3 实现真正的私有属性(ES6 Class 之前)
    4. 1.4. 4. WeakSet:值弱引用,不可重复
    5. 1.5. 5. 对比总结
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)