A user_4a13383d 2026-09-08 09:35 1 次浏览 Javascript Map、Set、WeakMap、WeakSet的区别和应用 在 JavaScript 中,Map、Set、WeakMap 和 WeakSet 是 ES6 引入的四种新型数据结构,它们各自解决了不同场景下的数据组织问题,核心差异体现在键的类型、引用方式以及是否可迭代三个方面。 ## 目录 - [一、先建立整体认识](#一先建立整体认识) - [二、Map:用任意类型的键保存键值对](#二map用任意类型的键保存键值对) - [三、Set:只保存不重复的值](#三set只保存不重复的值) - [四、WeakMap 和 WeakSet:弱引用集合](#四weakmap-和-weakset弱引用集合) - [五、四种集合的核心区别](#五四种集合的核心区别) - [六、应用一:Map 做缓存和计数器](#六应用一map-做缓存和计数器) - [七、应用二:Set 去重和集合运算](#七应用二set-去重和集合运算) - [八、应用三:WeakMap 保存对象私有数据](#八应用三weakmap-保存对象私有数据) - [九、应用四:WeakSet 标记已处理对象](#九应用四weakset-标记已处理对象) - [十、边界问题与选择建议](#十边界问题与选择建议) - [十一、总结](#十一总结) ## 一、先建立整体认识 普通对象也能保存键值对,但它的键最终会被转换成字符串或 Symbol: ```javascript const object = {}; const objectKey = { id: 1 }; object[objectKey] = '订单'; console.log(object['[object Object]']); // 订单 ``` 对象键名被“压扁”成了字符串。换成 Map,对象、函数、数字都可以作为真正的键: ```javascript const map = new Map(); const objectKey = { id: 1 }; map.set(objectKey, '订单'); console.log(map.get(objectKey)); // 订单 console.log(map.get({ id: 1 })); // undefined ``` 最后一行是关键:两个对象即使内容一样,只要不是同一个引用,就不是同一个键。 四种集合可以先这样记: | 集合 | 保存什么 | 键或元素限制 | 可否遍历 | 是否弱引用 | | --- | --- | --- | --- | --- | | Map | 键值对 | 键可以是任意类型 | 可以 | 否 | | Set | 唯一值 | 值可以是任意类型 | 可以 | 否 | | WeakMap | 对象键值对 | 键必须是对象或非注册 Symbol | 不可直接遍历 | 是 | | WeakSet | 对象集合 | 元素必须是对象或非注册 Symbol | 不可直接遍历 | 是 | ## 二、Map:用任意类型的键保存键值对 ### 1. 基本操作 ```javascript const userMap = new Map([ ['name', 'Ada'], ['age', 36], ]); userMap.set('role', 'admin'); console.log(userMap.get('name')); // Ada console.log(userMap.has('age')); // true console.log(userMap.size); // 3 userMap.delete('age'); userMap.clear(); ``` Map 的常用方法: - set(key, value):新增或覆盖键值。 - get(key):读取值。 - has(key):判断键是否存在。 - delete(key):删除一个键。 - clear():清空集合。 - size:读取元素数量。 ### 2. Map 可以使用任意类型的键 ```javascript const map = new Map(); const objectKey = { id: 1 }; const functionKey = () => {}; map.set(objectKey, '对象对应的数据'); map.set(functionKey, '函数对应的数据'); map.set(1, '数字键'); map.set(NaN, 'NaN 也能作为键'); console.log(map.get(objectKey)); // 对象对应的数据 console.log(map.get(functionKey)); // 函数对应的数据 ``` ### 3. Map 的遍历顺序 Map 会按照键值对加入的顺序遍历: ```javascript const map = new Map([ ['first', 1], ['second', 2], ]); for (const [key, value] of map) { console.log(key, value); } ``` 它还提供 keys()、values() 和 entries() 三个迭代器。 ## 三、Set:只保存不重复的值 ### 1. 基本操作 ```javascript const tags = new Set(['JavaScript', 'CSS', 'JavaScript']); tags.add('HTML'); console.log(tags); // Set(3) {'JavaScript', 'CSS', 'HTML'} console.log(tags.has('CSS')); // true console.log(tags.size); // 3 tags.delete('CSS'); ``` 重复值不会新增。Set 使用类似 Object.is 的规则判断重复值,因此 NaN 和 NaN 会被视为同一个值,+0 和 -0 也视为同一个值。 ### 2. 数组去重 ```javascript const numbers = [1, 2, 2, 3, 3, 3]; const uniqueNumbers = [...new Set(numbers)]; console.log(uniqueNumbers); // [1, 2, 3] ``` ### 3. Set 的边界:对象不会按内容去重 ```javascript const users = new Set([{ id: 1 }, { id: 1 }]); console.log(users.size); // 2 ``` 两个对象是两个不同的引用,Set 不会替你做深度比较。需要按 id 去重时,要自己提供规则: ```javascript const users = [ { id: 1, name: 'Ada' }, { id: 1, name: 'Grace' }, ]; const uniqueUsers = [ ...new Map(users.map((user) => [user.id, user])).values(), ]; console.log(uniqueUsers); // [{ id: 1, name: 'Grace' }] ``` ## 四、WeakMap 和 WeakSet:弱引用集合 ### 1. 什么是弱引用 普通 Map 会牢牢持有对象键。即使业务代码已经不再使用该对象,只要 Map 还保存着它,垃圾回收器就不能回收这块内存: ```javascript const cache = new Map(); let request = { url: '/api/user' }; cache.set(request, { parsed: true }); request = null; // cache 仍然持有原来的对象键 ``` WeakMap 不会因为保存对象键而阻止垃圾回收。对象没有其他强引用后,键和值都可以被回收。回收何时发生由 JavaScript 引擎决定,代码不能主动观察或强制指定时间。 ### 2. WeakMap 的限制 ```javascript const weakMap = new WeakMap(); const key = {}; weakMap.set(key, 'metadata'); console.log(weakMap.get(key)); // metadata console.log(weakMap.has(key)); // true ``` 它只支持 get、set、has 和 delete,没有 size、keys 或 for...of。这是故意的:如果可以遍历,就能通过集合观察某个对象什么时候被垃圾回收,反而破坏了弱引用的设计。 WeakMap 的键必须是对象或非注册 Symbol,不能使用字符串、数字等原始值: ```javascript const weakMap = new WeakMap(); // weakMap.set('user', 1); // TypeError ``` ### 3. WeakSet WeakSet 只保存对象或非注册 Symbol,不保存额外的值: ```javascript const visited = new WeakSet(); const node = {}; visited.add(node); console.log(visited.has(node)); // true visited.delete(node); ``` 它同样不可遍历,也没有 size。适合回答“这个对象是否已经处理过”,不适合统计“集合里一共有多少个对象”。 ## 五、四种集合的核心区别 | 对比项 | Map | Set | WeakMap | WeakSet | | --- | --- | --- | --- | --- | | 存储结构 | 键值对 | 值 | 对象键值对 | 对象值 | | 键/值类型 | 任意 | 任意 | 键是对象或非注册 Symbol | 元素是对象或非注册 Symbol | | 是否允许重复 | 键不重复 | 元素不重复 | 键不重复 | 元素不重复 | | 是否可遍历 | 可以 | 可以 | 不可以 | 不可以 | | 是否有 size | 有 | 有 | 没有 | 没有 | | 是否阻止对象回收 | 会 | 会 | 不会 | 不会 | | 典型用途 | 缓存、索引、计数 | 去重、成员判断 | 对象元数据、缓存 | 标记访问状态 | ## 六、应用一:Map 做缓存和计数器 ### 1. 对象缓存 当缓存键不是字符串,而是对象本身时,Map 很自然: ```javascript const resultCache = new Map(); function calculateTotal(order) { if (resultCache.has(order)) { return resultCache.get(order); } const total = order.items.reduce( (sum, item) => sum + item.price * item.quantity, 0, ); resultCache.set(order, total); return total; } const order = { items: [ { price: 10, quantity: 2 }, { price: 5, quantity: 1 }, ], }; console.log(calculateTotal(order)); // 25 console.log(calculateTotal(order)); // 25,命中缓存 ``` ### 拆解对象缓存 第一次调用 `calculateTotal(order)` 时,执行顺序是: 1. `resultCache.has(order)` 返回 `false`,说明这个订单对象还没有缓存。 2. `reduce` 从 `0` 开始,依次计算 `10 × 2` 和 `5 × 1`,得到总价 `25`。 3. `resultCache.set(order, 25)` 用订单对象的引用作为键保存结果。 4. 第二次传入同一个 `order` 引用时,`has` 返回 `true`,函数直接返回缓存,不再执行 `reduce`。 如果重新创建一个内容相同但引用不同的对象,仍然会重新计算,因为 Map 比较对象键时看的是引用身份,不是对象内容。 如果订单对象生命周期很短,而且缓存不应该影响回收,可以把 Map 换成 WeakMap: ```javascript const resultCache = new WeakMap(); ``` ### 2. 统计词频 ```javascript function countWords(words) { const counts = new Map(); for (const word of words) { counts.set(word, (counts.get(word) ?? 0) + 1); } return counts; } console.log(countWords(['js', 'css', 'js'])); // Map(2) {'js' => 2, 'css' => 1} ``` ## 七、应用二:Set 去重和集合运算 ### 1. 权限判断 ```javascript const permissions = new Set(['read', 'write']); function canEdit() { return permissions.has('write'); } console.log(canEdit()); // true ``` ### 2. 计算交集和差集 ```javascript const frontend = new Set(['JavaScript', 'CSS', 'HTML']); const backend = new Set(['JavaScript', 'Node.js']); const common = new Set([...frontend].filter((item) => backend.has(item))); const onlyFrontend = new Set( [...frontend].filter((item) => !backend.has(item)), ); console.log(common); // Set {'JavaScript'} console.log(onlyFrontend); // Set {'CSS', 'HTML'} ``` ## 八、应用三:WeakMap 保存对象私有数据 WeakMap 常用来保存不希望暴露在对象上的元数据。对象销毁后,对应元数据也可以一起回收: ```javascript const privateData = new WeakMap(); class User { constructor(name) { privateData.set(this, { loginCount: 0, secret: '不能从实例上直接看到', }); this.name = name; } login() { const data = privateData.get(this); data.loginCount += 1; } getLoginCount() { return privateData.get(this).loginCount; } } const user = new User('Ada'); user.login(); console.log(user.getLoginCount()); // 1 console.log(user.secret); // undefined ``` 现代 JavaScript 也可以使用 #privateField 实现私有字段,但 WeakMap 适合需要兼容旧语法、或希望把元数据逻辑放在类外部的场景。 ## 九、应用四:WeakSet 标记已处理对象 处理树、图或递归数据时,WeakSet 可以记录访问过的对象,避免循环引用导致无限递归: ```javascript function printTree(node, visited = new WeakSet()) { if (node === null || typeof node !== 'object') { return; } if (visited.has(node)) { console.log('发现已经访问过的节点,停止继续走'); return; } visited.add(node); console.log(node.name); for (const child of node.children ?? []) { printTree(child, visited); } } const root = { name: 'root', children: [] }; const child = { name: 'child', children: [] }; root.children.push(child); child.children.push(root); printTree(root); // root // child // 发现已经访问过的节点,停止继续走 ``` ### 追踪 `printTree` 的递归 访问 `root` 时,`visited` 的变化是: | 调用 | `visited` 操作 | 后续动作 | | --- | --- | --- | | `printTree(root)` | `root` 不在集合,加入 | 打印 `root`,继续访问 `child` | | `printTree(child)` | `child` 不在集合,加入 | 打印 `child`,继续访问 `root` | | `printTree(root)` | 命中已访问的 `root` | 打印提示并 `return`,不再进入子节点 | `visited.add(node)` 必须放在递归子节点之前。若等所有子节点处理完才登记,`root → child → root` 的环会在登记前反复绕圈,递归就没有刹车片了。 这里用 WeakSet 而不是普通数组,是因为我们只关心“是否访问过”,不需要保存额外值,也不希望访问记录长期持有节点对象。 ## 十、边界问题与选择建议 ### 1. 不要把 WeakMap 当成可遍历 Map WeakMap 没有 size 和遍历能力。如果业务需要展示、统计或批量清理集合内容,使用 Map,并自行管理生命周期。 ### 2. 不要为了“高级”而使用弱引用 如果键是字符串、数字或其他原始值,只能使用 Map;如果需要知道集合大小,也不能使用 WeakMap 或 WeakSet。 ### 3. 弱引用不等于立即删除 垃圾回收时机由引擎决定。不要写出“对象设为 null 后,WeakMap 马上少一个元素”这样的逻辑,因为弱集合不能被遍历,也没有可观察的数量变化。 ### 4. Map 和对象不是简单的谁替代谁 - 需要动态增删、任意类型键或明确的插入顺序:优先 Map。 - 需要 JSON 序列化、结构简单且键就是字符串:普通对象通常更直接。 - 需要唯一值和快速成员判断:优先 Set。 - 只给对象挂元数据、缓存或访问标记,且不希望阻止回收:考虑 WeakMap 或 WeakSet。 ## 十一、总结 - Map 保存键值对,键可以是任意类型;Set 保存不重复的值。 - WeakMap 和 WeakSet 只面向对象关联数据,并且不可遍历、没有 size。 - Map/Set 会持有对象引用;WeakMap/WeakSet 不会阻止对象被垃圾回收。 - Map 适合缓存、索引和计数;Set 适合去重、权限和集合运算。 - WeakMap 适合对象私有数据和对象缓存;WeakSet 适合标记已访问对象。 - 选择集合前先问三件事:要不要键值对、要不要遍历、是否允许集合持有对象生命周期。 完整案例见 examples/mapSetWeakMapWeakSet.html。 点赞0 收藏0
评论
还没有评论,来分享你的想法吧。