Map / Set / WeakMap / WeakSet

系列文章目录

《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)


文章目录

  • 系列文章目录
  • 前言
  • 一、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 新增的四种集合类型MapSetWeakMapWeakSet

它们解决的核心问题是 ObjectArray 在特定场景下的不足:

  • Object 的键只能是字符串(或 Symbol),数字键会被隐式转换
  • Object 没有直接获取大小的方法,需要 Object.keys(obj).length
  • Object 无法直接遍历,需要借助 for...inObject.entries
  • 普通对象作为缓存时,无法被垃圾回收

MapSet 提供了更完善的数据结构,WeakMapWeakSet 则引入了弱引用,解决了内存泄漏问题。


一、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 === NaNtrue

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 在需要遍历、统计、序列化的场景下更合适。


六、易混淆点

  1. Map 的键比较用 SameValueZeroNaN === NaNtrue+0 === -0true
  2. Map 和 Object 的键类型不同:Map 可以用任意类型,Object 只能用字符串/Symbol。
  3. WeakMap/WeakSet 的"弱"是针对键/值对象,不是针对 WeakMap/WeakSet 本身。
  4. WeakMap 的键只能是对象:原始值(数字、字符串等)不能作为 WeakMap 的键。
  5. Set 没有键set.keys()set.values() 行为相同。
  6. Map 可以转数组,WeakMap 不行:因为 WeakMap 不可遍历。
  7. 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 Proxyref / shallowRef(系列第 34 篇,大纲 §34)。

相关推荐
AlienZHOU9 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc12 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师14 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen14 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒14 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow15 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81815 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007815 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明15 小时前
第 6 章 组件进阶
前端·vue.js
江华森15 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端