第60课:Map、Set 与 Weak 版本——集合类型、WeakMap 用途、内存泄漏防护
在 ES6 之前,JavaScript 中管理键值对集合只能依赖普通对象 {}(键只能是字符串或 Symbol),而管理不重复的值集合则需手动通过数组模拟。ES6 引入了四个新的集合类型:**Map(键值对集合,键可以是任意类型)、Set(唯一值集合)、WeakMap(键必须是对象且被弱引用的 Map)和 WeakSet(值必须是对象且被弱引用)。它们各自解决了特定场景下的痛点,尤其是 WeakMap 在私有数据存储和内存泄漏防护**方面有不可替代的作用。本节课将深入每种集合类型的 API、使用场景和性能特性,并对比它们与普通对象的区别。
1. Map:键值对集合,键可以是任意类型
1.1 Map 与 Object 的核心区别
| 特性 | Map |
Object |
|---|---|---|
| 键的类型 | 任意值:对象、函数、基本类型均可。 | 仅字符串或 Symbol。 |
| 键的顺序 | 严格按插入顺序。 | 字符串键按插入顺序(ES2015+),Symbol 键在最后。 |
| 大小获取 | map.size 属性直接获取。 |
需 Object.keys(obj).length 手动计算。 |
| 迭代 | 直接可迭代:for-of、forEach、entries() 等。 |
需 Object.entries(obj) 转换后迭代。 |
| 性能 | 频繁增删键值对时更优。 | 适合少量固定键,频繁增删时性能不如 Map。 |
| 原型链干扰 | 无原型属性污染,纯数据容器。 | 继承 Object.prototype,键可能被 toString 等污染。 |
何时选择 Map:键的类型不限于字符串、需要保持插入顺序、需要频繁增删、需要直接获取大小、需要避免原型污染。
1.2 创建与基本操作
1 | const map = new Map([ |
1.3 遍历 Map
1 | const map = new Map([['a', 1], ['b', 2], ['c', 3]]); |
1.4 Map 键的相等性判断
Map 使用 SameValueZero 算法判断键是否相等。与 === 的主要区别是:NaN 被视为与自身相等(Map 允许 NaN 作为键),且 +0 和 -0 视为相等。
1 | const map = new Map(); |
2. Set:唯一值集合
Set 存储不重复的值,值的相等性同样基于 SameValueZero 算法。
2.1 基本操作
1 | const set = new Set([1, 2, 2, 3, 3, 4]); |
2.2 数组去重与集合运算
Set 与数组之间的转换非常简便,常用于去重。
1 | const arr = [1, 1, 2, 3, 3, 4]; |
集合运算(交集、并集、差集)可通过 Set 与数组方法的组合实现:
1 | const a = new Set([1, 2, 3]); |
2.3 遍历 Set
Set 同样支持 for-of、forEach、keys()、values()(keys() 和 values() 行为一致,返回值的迭代器)和 entries()(返回 [value, value] 以保持与 Map 接口一致)。
1 | const set = new Set(['apple', 'banana', 'cherry']); |
3. WeakMap:键弱引用,自动垃圾回收
WeakMap 是一种特殊的 Map,其键必须是对象,且该引用是弱引用——如果外部没有其他地方引用该键对象,垃圾回收器可以将其回收,对应的键值对也会从 WeakMap 中自动移除。WeakMap 不可迭代,没有 size 属性,没有 clear() 方法,无法遍历键或值。
3.1 基本 API
1 | const weakMap = new WeakMap(); |
当 obj = null 后,该对象仅被 WeakMap 弱引用,垃圾回收器将自动回收该键值对。开发者无法观察这一过程(因为没有迭代器),但这正是 WeakMap 的价值——不会阻止垃圾回收。
3.2 典型用途:存储关联的私有数据
WeakMap 常用于存储与 DOM 元素或对象实例绑定的数据,当 DOM 元素从文档中移除且无其他引用时,相关数据会被自动清理,避免内存泄漏。
1 | // 存储每个 DOM 节点的点击次数 |
3.3 实现真正的私有属性(ES6 Class 之前)
在 ES2022 私有字段(#)出现之前,WeakMap 是模拟类实例私有变量的可靠手段。
1 | const _private = new WeakMap(); |
4. WeakSet:值弱引用,不可重复
WeakSet 是 Set 的弱引用版本,其值必须是对象,且是弱引用。同样不可迭代,没有 size。主要用于标记对象的状态(如“是否已处理”),当对象被销毁时,标记自动消失。
1 | const processedItems = new WeakSet(); |
与使用 Set 存储对象相比,WeakSet 不会阻止对象被垃圾回收,适合作为对象生命周期的标记。
5. 对比总结
| 类型 | 键/值要求 | 迭代性 | 弱引用 | 典型场景 |
|---|---|---|---|---|
Map |
键任意类型 | 可迭代 | 否 | 通用键值存储,替代普通对象。 |
Set |
值任意类型 | 可迭代 | 否 | 去重、集合运算。 |
WeakMap |
键必须是对象 | 不可迭代 | 是 | 绑定外部对象的元数据,防止内存泄漏。 |
WeakSet |
值必须是对象 | 不可迭代 | 是 | 标记对象状态(如已访问、已处理)。 |
选择原则:
- 如果键值对中的键需要使用对象本身,且希望在对象被销毁后自动清理,用
WeakMap。 - 如果需要存储不重复的值列表,用
Set。 - 如果仅需标记对象而不存储额外数据,用
WeakSet。 - 通用键值存储用
Map。
课后练习
一、概念自测(选择题 / 填空题)
(单选) 在
Map中,键的相等性判断使用什么算法?
A.===
B.Object.is
C. SameValue
D. SameValueZero(单选)
WeakMap的键必须是?
A. 字符串
B. 对象
C. Symbol
D. 任意类型(填空)
Set对象内部值不重复,可以用来快速实现数组去重:const unique = [______ new Set(arr)]。(多选) 以下哪些操作在
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保证迭代顺序与插入顺序一致,get和put访问时需要将其删除再重新插入以更新顺序)。 - 使用
class语法封装,添加 JSDoc 注释说明容量限制和 LRU 算法。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定 Map 的插入顺序特性如何用于 LRU、get 和 put 的实现逻辑。
三、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 或现代浏览器环境中运行。
课后练习答案
一、概念自测答案
D
- 解析:
Map使用 SameValueZero 算法,NaN与自身相等,+0与-0相等。
- 解析:
B
- 解析:
WeakMap的键必须是对象(或非 null 的 Object 类型),不能是基本类型。
- 解析:
**
...**(展开运算符)- 解析:
[...new Set(arr)]或Array.from(new Set(arr))均可实现去重。
- 解析:
B、D
- 解析:
WeakMap没有size属性且不可迭代,因此forEach也不可用。get和has是可用的。
- 解析:
二、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 导出。直接输出完整代码。”