系列文章目录
《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)
- 第 01 篇:数据类型与类型判断
- 第 02 篇:变量声明与作用域
- 第 03 篇:闭包与高阶函数
- 第 04 篇:函数工厂
- 第 05 篇:this 指向与绑定
- 第 06 篇:原型与原型链
- 第 07 篇:类与继承
- 第 08 篇:JS 执行机制与异步队列
- 第 09 篇:数组常用方法
- 第 10 篇:字符串算法
- 第 11 篇:常见手写题合集(上)
- 第 12 篇:常见手写题合集(下)
- 第 13 篇:Promise 与 async/await
- 第 14 篇:数据结构基础
- 第 15 篇:垃圾回收与内存
- 第 16 篇:DOM 基础全面解析
- 第 17 篇:DOM 性能与渲染
- 第 18 篇:DOM 交互补充
- 第 19 篇:DOM 实战案例
- 第 20 篇:CSS 布局与可视化高频
- 第 21 篇:移动端与 viewport
- 第 22 篇:BOM 核心对象
- 第 23 篇:前端路由原理
- 第 24 篇:浏览器存储对比
- 第 25 篇:网络与跨域
- 第 26 篇:网络请求与实时通道
- 第 27 篇:Service Worker、PWA 与 Web Worker
- 第 28 篇:浏览器高级 API
- 第 29 篇:图片懒加载
- 第 30 篇:ES6+ 模块
- 第 31 篇:Symbol 与 Iterator / Generator
- 第 32 篇:Proxy 与 Reflect
- 第 33 篇:Map / Set / WeakMap / WeakSet(本文)
文章目录
- 系列文章目录
- 前言
- 一、Map:任意类型的键
-
- [1.1 为什么需要 Map](#1.1 为什么需要 Map)
- [1.2 Map 的基本 API](#1.2 Map 的基本 API)
- [1.3 Map 的键比较:SameValueZero](#1.3 Map 的键比较:SameValueZero)
- [1.4 Map 的遍历](#1.4 Map 的遍历)
- [1.5 Map 与 Object 的选型](#1.5 Map 与 Object 的选型)
- 二、Set:唯一值的集合
-
- [2.1 基本用法](#2.1 基本用法)
- [2.2 去重场景](#2.2 去重场景)
- [2.3 集合运算](#2.3 集合运算)
- [2.4 Set 的遍历](#2.4 Set 的遍历)
- [三、WeakMap:弱引用的 Map](#三、WeakMap:弱引用的 Map)
-
- [3.1 为什么需要 WeakMap](#3.1 为什么需要 WeakMap)
- [3.2 WeakMap 的限制](#3.2 WeakMap 的限制)
- [3.3 DOM 元数据关联](#3.3 DOM 元数据关联)
- [3.4 私有数据](#3.4 私有数据)
- [四、WeakSet:弱引用的 Set](#四、WeakSet:弱引用的 Set)
-
- [4.1 基本特性](#4.1 基本特性)
- [4.2 标记已处理对象](#4.2 标记已处理对象)
- [4.3 循环引用检测](#4.3 循环引用检测)
- 五、设计层面的思考
-
- [5.1 为什么 WeakMap/WeakSet 不可遍历](#5.1 为什么 WeakMap/WeakSet 不可遍历)
- [5.2 弱引用不是"自动删除"](#5.2 弱引用不是"自动删除")
- [5.3 为什么不直接用 WeakMap 替代 Map](#5.3 为什么不直接用 WeakMap 替代 Map)
- 六、易混淆点
- 七、思考与练习
- 总结
前言
上一篇讲了 Proxy 能拦截对象操作;本篇讲 ES6 新增的四种集合类型 :Map、Set、WeakMap、WeakSet。
它们解决的核心问题是 Object 和 Array 在特定场景下的不足:
Object的键只能是字符串(或 Symbol),数字键会被隐式转换Object没有直接获取大小的方法,需要Object.keys(obj).lengthObject无法直接遍历,需要借助for...in或Object.entries- 普通对象作为缓存时,无法被垃圾回收
Map 和 Set 提供了更完善的数据结构,WeakMap 和 WeakSet 则引入了弱引用,解决了内存泄漏问题。
一、Map:任意类型的键
1.1 为什么需要 Map
javascript
const obj = {};
// 问题1:数字键被隐式转换为字符串
obj[1] = "one";
obj["1"] = "one"; // 覆盖了上面的值
console.log(obj); // { "1": "one" }
// 问题2:对象作为键会变成 "[object Object]"
const key = { id: 1 };
obj[key] = "value";
console.log(obj); // { "1": "one", "[object Object]": "value" }
// 问题3:无法直接获取大小
Object.keys(obj).length; // 需要额外计算
Map 解决了所有这些问题:
javascript
const map = new Map();
// 键可以是任意类型
map.set(1, "one");
map.set("1", "string one");
console.log(map.get(1)); // "one"
console.log(map.get("1")); // "string one"
// 对象作为键不会被转换
const keyObj = { id: 1 };
map.set(keyObj, "object value");
console.log(map.get(keyObj)); // "object value"
// 直接获取大小
console.log(map.size); // 3
1.2 Map 的基本 API
javascript
const map = new Map();
// 添加/修改
map.set("name", "Alice");
map.set("age", 25);
// 读取
map.get("name"); // "Alice"
map.get("unknown"); // undefined
// 判断
map.has("name"); // true
map.has("email"); // false
// 删除
map.delete("age"); // true
map.delete("email"); // false(不存在)
// 大小
map.size; // 1
// 清空
map.clear();
map.size; // 0
1.3 Map 的键比较:SameValueZero
Map 的键使用 SameValueZero 算法比较,和 === 基本相同,但 NaN === NaN 为 true:
javascript
const map = new Map();
map.set(NaN, "not a number");
console.log(map.get(NaN)); // "not a number"
// +0 和 -0 被视为相同
map.set(+0, "zero");
console.log(map.get(-0)); // "zero"
1.4 Map 的遍历
Map 维护插入顺序,可以直接遍历:
javascript
const map = new Map([
["a", 1],
["b", 2],
["c", 3]
]);
// for...of
for (const [key, value] of map) {
console.log(key, value);
}
// forEach
map.forEach((value, key) => {
console.log(key, value);
});
// 转为数组
[...map]; // [["a",1], ["b",2], ["c",3]]
[...map.keys()]; // ["a", "b", "c"]
[...map.values()]; // [1, 2, 3]
[...map.entries()]; // [["a",1], ["b",2], ["c",3]]
1.5 Map 与 Object 的选型
| 对比项 | Map | Object |
|---|---|---|
| 键类型 | 任意类型 | 只能字符串/Symbol |
| 大小 | map.size |
Object.keys(obj).length |
| 遍历 | 直接 for...of |
需要 Object.entries 等 |
| 性能 | 频繁增删场景更优 | 固定结构更快 |
| 序列化 | 无内置 JSON 支持 | 直接 JSON.stringify |
经验法则:
- 键是固定且已知的 → 用对象
- 键会动态变化、需要非字符串键、需要频繁增删 → 用 Map
二、Set:唯一值的集合
2.1 基本用法
Set 是一个值的集合,其中每个值都是唯一的(基于 SameValueZero 判断)。
javascript
const set = new Set();
set.add(1);
set.add(2);
set.add(2); // 重复,忽略
set.add("1"); // 和 1 不同
console.log(set.size); // 3
console.log(set.has(1)); // true
console.log(set.has(3)); // false
set.delete(1);
console.log(set.has(1)); // false
2.2 去重场景
Set 最常见的用途是数组去重:
javascript
const arr = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(arr)]; // [1, 2, 3]
对于对象数组,Set 无法去重(因为引用不同):
javascript
const obj1 = { id: 1 };
const obj2 = { id: 1 };
const set = new Set([obj1, obj2]);
console.log(set.size); // 2,两个不同的引用
2.3 集合运算
利用 Set 实现集合运算:
javascript
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 diff = new Set([...a].filter(x => !b.has(x))); // {1}
2.4 Set 的遍历
Set 也维护插入顺序:
javascript
const set = new Set(["a", "b", "c"]);
for (const value of set) {
console.log(value); // "a", "b", "c"
}
set.forEach(value => {
console.log(value);
});
[...set]; // ["a", "b", "c"]
注意:Set 没有键,只有值 。keys() 和 values() 返回相同的迭代器:
javascript
const set = new Set([1, 2, 3]);
console.log(set.keys() === set.values()); // 行为上等价
三、WeakMap:弱引用的 Map
3.1 为什么需要 WeakMap
普通 Map 会强引用键对象,即使外部不再引用该对象,Map 仍然持有它,导致无法被垃圾回收:
javascript
let obj = { data: "important" };
const map = new Map();
map.set(obj, "metadata");
obj = null; // 外部不再引用
// 但 Map 仍然持有 obj 的引用,GC 无法回收
console.log(map.size); // 1
WeakMap 使用弱引用,当键对象不再被其他地方引用时,会被垃圾回收:
javascript
let obj = { data: "important" };
const weakMap = new WeakMap();
weakMap.set(obj, "metadata");
obj = null; // 外部不再引用
// WeakMap 的弱引用允许 GC 回收 obj
// 下次 GC 后,weakMap 中对应的条目会自动消失
3.2 WeakMap 的限制
WeakMap 只能用对象作为键 ,且不可遍历:
javascript
const weakMap = new WeakMap();
// 只能用对象键
weakMap.set({ id: 1 }, "value"); // OK
// weakMap.set(1, "value"); // TypeError
// 不可遍历
// for (const [key, value] of weakMap) {} // TypeError
// weakMap.keys(); // undefined
// weakMap.values(); // undefined
// 只有基本方法
weakMap.get(obj);
weakMap.set(obj, value);
weakMap.has(obj);
weakMap.delete(obj);
3.3 DOM 元数据关联
WeakMap 最经典的用途是给 DOM 元素附加元数据而不阻止 DOM 元素被移除:
javascript
const elementData = new WeakMap();
function setupElement(element) {
const data = { clickCount: 0, listeners: [] };
elementData.set(element, data);
element.addEventListener("click", () => {
data.clickCount++;
});
}
function cleanupElement(element) {
elementData.delete(element);
element.remove();
// 当 element 不再被引用时,WeakMap 中的条目会被 GC 回收
}
如果用普通 Map,即使 DOM 元素被移除,Map 仍然持有引用,导致内存泄漏。
3.4 私有数据
WeakMap 可以实现真正的私有属性:
javascript
const privateData = new WeakMap();
class User {
constructor(name, age) {
privateData.set(this, { name, age });
}
getName() {
return privateData.get(this).name;
}
getAge() {
return privateData.get(this).age;
}
}
const user = new User("Alice", 25);
console.log(user.getName()); // "Alice"
// 外部无法访问 privateData
四、WeakSet:弱引用的 Set
4.1 基本特性
WeakSet 和 WeakMap 类似:
- 只能存储对象
- 弱引用,对象可被 GC 回收
- 不可遍历
javascript
const weakSet = new WeakSet();
const obj1 = { id: 1 };
const obj2 = { id: 2 };
weakSet.add(obj1);
weakSet.add(obj2);
weakSet.add(obj1); // 重复,忽略
console.log(weakSet.has(obj1)); // true
weakSet.delete(obj1);
console.log(weakSet.has(obj1)); // false
4.2 标记已处理对象
WeakSet 最常见的用途是标记对象是否已处理:
javascript
const processed = new WeakSet();
function processNode(node) {
if (processed.has(node)) return; // 避免重复处理
processed.add(node);
// 处理逻辑...
}
4.3 循环引用检测
在深拷贝中,WeakSet 可以检测循环引用:
javascript
function deepClone(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== "object") return obj;
if (seen.has(obj)) {
return {}; // 循环引用,返回空对象或抛错
}
seen.add(obj);
const clone = Array.isArray(obj) ? [] : {};
for (const key of Object.keys(obj)) {
clone[key] = deepClone(obj[key], seen);
}
return clone;
}
五、设计层面的思考
5.1 为什么 WeakMap/WeakSet 不可遍历
如果 WeakMap/WeakSet 可以遍历,就需要在遍历过程中保持所有弱引用对象的存活(否则遍历时对象可能被回收,导致行为不确定)。这违背了弱引用的设计初衷。
5.2 弱引用不是"自动删除"
弱引用意味着键对象可以被 GC 回收 ,但 WeakMap/WeakSet 本身不会主动删除条目。条目只在 GC 回收键对象后才"消失",且这个时机是不确定的。
5.3 为什么不直接用 WeakMap 替代 Map
WeakMap 的限制很多:
- 键只能是对象
- 不可遍历
- 没有
size属性 - 没有
clear方法
Map 在需要遍历、统计、序列化的场景下更合适。
六、易混淆点
- Map 的键比较用 SameValueZero :
NaN === NaN为true,+0 === -0为true。 - Map 和 Object 的键类型不同:Map 可以用任意类型,Object 只能用字符串/Symbol。
- WeakMap/WeakSet 的"弱"是针对键/值对象,不是针对 WeakMap/WeakSet 本身。
- WeakMap 的键只能是对象:原始值(数字、字符串等)不能作为 WeakMap 的键。
- Set 没有键 :
set.keys()和set.values()行为相同。 - Map 可以转数组,WeakMap 不行:因为 WeakMap 不可遍历。
- WeakMap/WeakSet 不能保证条目何时消失:只保证在键对象被 GC 后会消失。
七、思考与练习
1. 以下代码输出什么?
javascript
const map = new Map();
map.set(1, "one");
map.set("1", "string one");
console.log(map.size);
解析:输出 2。Map 中 1 和 "1" 是不同的键。
2. 如何用 Map 实现一个简单的 LRU 缓存?
javascript
class LRUCache {
constructor(limit) {
this.limit = limit;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value); // 移到最后
return value;
}
put(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
if (this.cache.size >= this.limit) {
// 删除最久未使用的(第一个)
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, value);
}
}
3. 为什么用 WeakMap 存储 DOM 元素数据比 Map 更安全?
解析:DOM 元素被移除后,如果没有其他引用,WeakMap 的弱引用允许 GC 回收该元素,避免内存泄漏。Map 会保持强引用,即使 DOM 元素已从页面移除,仍然无法被回收。
4. 如何用 Set 判断两个数组是否有交集?
javascript
function hasIntersection(arr1, arr2) {
const set = new Set(arr1);
return arr2.some(item => set.has(item));
}
hasIntersection([1, 2, 3], [3, 4, 5]); // true
hasIntersection([1, 2], [3, 4]); // false
5. Object.entries() 和 new Map() 之间如何互转?
javascript
// Object → Map
const obj = { a: 1, b: 2 };
const map = new Map(Object.entries(obj));
// Map → Object
const obj2 = Object.fromEntries(map);
6. 为什么 WeakSet 适合存储"已访问"标记?
解析:当对象被销毁时,WeakSet 中的弱引用允许 GC 回收,不会造成内存泄漏。如果用普通 Set,即使对象不再被引用,Set 仍然持有它,导致内存泄漏。
总结
- Map :任意类型键、直接遍历、
size属性,适合动态键集合和频繁增删场景。 - Set:唯一值集合,常用于去重和集合运算。
- WeakMap:弱引用键对象,键只能是对象,不可遍历,适合 DOM 元数据关联和私有数据。
- WeakSet:弱引用对象,不可遍历,适合标记已处理对象和循环引用检测。
- 选择依据:需要遍历用 Map/Set,需要弱引用用 WeakMap/WeakSet。
下一篇讲 Vue 响应式原理 :依赖收集与派发更新、Object.defineProperty vs Proxy、ref / shallowRef(系列第 34 篇,大纲 §34)。