Map/Set/WeakMap/WeakSet学习笔记

一、核心概念与区别总览

特性 Set Map WeakSet WeakMap
本质 值的集合(唯一) 键值对的集合 对象的弱引用集合 对象的弱引用键值对
键/值类型 任意类型 任意类型 只能是对象 键必须是对象,值任意
重复性 不可重复 键不可重复 不可重复 键不可重复
引用类型 强引用 (阻止GC) 强引用 (阻止GC) 弱引用 (不阻止GC) 弱引用 (不阻止GC)
可遍历性 支持 (for...of, keys等) 支持 (for...of, keys等) 不可遍历 不可遍历
size 属性 有 有 无 无
主要用途 去重、集合运算 字典、缓存、关联数据 临时对象标记、私有属性 缓存、DOM节点关联数据

二、详细解析与常用 API

1. Set (集合)

核心特点:成员唯一,无序(但按插入顺序遍历)。

常用 API:

javascript 复制代码
const mySet = new Set([1, 2, 2, 3]); // Set(3) {1, 2, 3}

// 增
mySet.add(4); 

// 删
mySet.delete(2); 

// 查
mySet.has(3); // true
mySet.size;   // 3 (注意是属性不是方法)

// 遍历
mySet.forEach((value) => console.log(value));
for (let item of mySet) { console.log(item); }

// 转数组 (面试常考:数组去重)
const arr = [...mySet]; 
// 或 Array.from(mySet)

面试考点 & 实战:

  • 数组去重 :[...new Set(array)] 是最简洁的去重方式。

  • 集合运算 :利用 Set 实现交集、并集、差集。

    javascript 复制代码
    const a = new Set([1, 2, 3]);
    const b = new Set([3, 4, 5]);
    
    // 并集
    const union = new Set([...a, ...b]); 
    // 交集
    const intersection = new Set([...a].filter(x => b.has(x)));
    // 差集 (a中有但b中没有)
    const difference = new Set([...a].filter(x => !b.has(x)));
  • NaN 的处理 :Set 中 NaN 被视为相等,只存一个。const s = new Set([NaN, NaN]); // size: 1

2. Map (字典)

核心特点 :键值对,键可以是任意类型(包括对象、函数),保留插入顺序。

常用 API:

javascript 复制代码
const myMap = new Map();
const objKey = { id: 1 };

// 增/改
myMap.set('key1', 'value1');
myMap.set(objKey, 'object value'); // 对象作键

// 查
myMap.get('key1'); 
myMap.get(objKey); 
myMap.has('key1');
myMap.size;

// 删
myMap.delete('key1');
myMap.clear();

// 遍历 (比对象更灵活)
for (let [key, value] of myMap) { ... }
myMap.forEach((value, key) => { ... });

面试考点 & 实战:

  • Map vs Object :
    • 键类型:Object 的键只能是字符串或 Symbol;Map 可以是任意类型。
    • 顺序:Map 保证插入顺序;Object (ES6+) 虽也有顺序但主要设计为哈希表。
    • 大小 :Map 有 size 属性;Object 需手动计算。
    • 性能:频繁增删键值对时,Map 通常优于 Object。
    • 序列化 :Object 原生支持 JSON;Map 需转换 (JSON.stringify([...map]))。
  • 应用场景 :
    • 需要非字符串键(如用 DOM 节点做键存储相关数据)。
    • 需要频繁增删改查的大型数据集。
    • 维护一个有序的键值对列表。
3. WeakSet & WeakMap (弱引用)

核心特点 :弱引用。如果对象没有其他引用指向它,垃圾回收机制 (GC) 会自动回收该对象,集合中的对应项也会自动消失。

关键限制:

  1. 只能存对象(不能存原始类型)。
  2. 不可遍历 (没有 keys(), values(), entries(), forEach)。
  3. 没有 size 属性(因为元素可能随时被 GC 回收,大小不确定)。

面试考点 & 实战:

  • 防止内存泄漏 :这是最大的考点。

    • 场景 :给 DOM 元素绑定临时数据。如果使用 Map,即使 DOM 元素从页面移除,只要 Map 还持有引用,该元素就无法被 GC,导致内存泄漏。使用 WeakMap,当 DOM 元素被移除且无其他引用时,自动从 WeakMap 中清除。
  • 私有属性模拟 :

    javascript 复制代码
    const _privateData = new WeakMap();
    
    class MyClass {
      constructor() {
        _privateData.set(this, { secret: '123' });
      }
      getSecret() {
        return _privateData.get(this).secret;
      }
    }
    // 外部无法直接访问 _privateData 中的内容,且实例销毁后自动清理
  • 缓存优化:用于缓存计算结果,当原对象被回收时,缓存也自动失效,避免缓存无限增长。


三、WeakMap、WeakSet使用场景

在日常的业务逻辑开发(如写页面交互、处理数据列表)中,确实很少直接显式地 new WeakMap() 或 new WeakSet()。

大多数时候,我们直接用 Map、Object 或者数组就能解决问题。

但是,"没用到"不代表"不重要" 。WeakMap 和 WeakSet 通常隐藏在底层库、框架源码、性能优化方案 以及防止内存泄漏的架构设计中。

以下是它们的具体用途:

1. 核心场景:防止内存泄漏(最经典用途)

这是 WeakMap 存在的最大意义。当你需要将数据与某个生命周期不确定 的对象(如 DOM 节点、第三方库生成的对象)绑定时,必须用 WeakMap。

场景描述:

假设你正在开发一个复杂的 Dashboard,页面上有成千上万个图表组件。你需要给每个 DOM 节点绑定一些私有数据(比如配置项、状态缓存)。

  • 如果用 Map / 全局对象:

    javascript 复制代码
    const nodeDataMap = new Map();
    
    function initChart(domNode) {
      const data = { config: {...}, state: 'active' };
      nodeDataMap.set(domNode, data); // 强引用
    }
    
    function destroyChart(domNode) {
      // ⚠️ 危险点:如果你忘记调用 delete,或者逻辑复杂漏删了
      // nodeDataMap.delete(domNode); 
      
      // 即使 domNode 从页面移除了(用户关闭了标签页),
      // 只要 nodeDataMap 还活着,domNode 就永远无法被 GC 回收!
      // 结果:内存泄漏,页面越来越卡,最终崩溃。
    }
  • 如果用 WeakMap:

    javascript 复制代码
    const nodeDataMap = new WeakMap(); // 弱引用
    
    function initChart(domNode) {
      const data = { config: {...}, state: 'active' };
      nodeDataMap.set(domNode, data); 
      // 这里只是"暂时"关联。
    }
    
    function destroyChart(domNode) {
      // ✅ 安全:不需要手动 delete!
      // 当 domNode 从 DOM 树移除,且代码中没有其他变量引用它时,
      // 浏览器 GC 会自动回收 domNode。
      // 一旦 domNode 被回收,WeakMap 中对应的 entry 也会自动消失。
      // 内存自动释放,零泄漏风险。
    }

实际案例:

  • DOM 库封装 :很多 UI 库(如早期的 jQuery 插件,或现在的自定义指令)内部用 WeakMap 存储实例数据。
  • 事件监听器管理:存储元素对应的事件处理函数映射。

2. 核心场景:实现"私有属性" (Private Properties)

在 ES6 class 的 #privateField 语法出现之前,WeakMap 是实现真正私有属性的唯一标准方案(比闭包更灵活,比 _variable 命名约定更安全)。

即使现在有了 # 语法,某些动态场景或 Babel 转译后的代码依然依赖 WeakMap。

代码示例:
javascript 复制代码
const _secretData = new WeakMap();

class User {
  constructor(name, token) {
    this.name = name;
    // 将敏感数据存储在 WeakMap 中,key 是当前实例(this)
    _secretData.set(this, { token: token, role: 'admin' });
  }

  getToken() {
    return _secretData.get(this).token;
  }
}

const u = new User('Alice', 'xyz-123');
console.log(u.token); // undefined (外部无法访问)
console.log(u._secretData); // undefined (根本挂不在实例上)
// 只有类内部通过 _secretData.get(this) 才能拿到

优势:

  • 不可枚举 :Object.keys(u) 看不到这些数据。
  • 自动清理 :当 u 被销毁,敏感数据也随之消失,不会残留在内存中。

3. 核心场景:高性能缓存 (Memoization)

当你需要缓存函数的计算结果,但输入参数是对象 时,WeakMap 是完美的缓存容器。

痛点:

如果你用普通 Map 做缓存:

javascript 复制代码
const cache = new Map();
function heavyCalc(obj) {
  if (cache.has(obj)) return cache.get(obj);
  const res = doSomething(obj);
  cache.set(obj, res); 
  return res;
}
// 问题:如果 obj 在业务中不再使用,但 cache 里还存着,导致 obj 无法回收。
// 随着时间推移,cache 会无限膨胀,吃掉所有内存。
解决方案:
javascript 复制代码
const cache = new WeakMap(); // 弱引用缓存
function heavyCalc(obj) {
  if (cache.has(obj)) return cache.get(obj);
  const res = doSomething(obj);
  cache.set(obj, res);
  return res;
}
// 优势:一旦外部的 obj 被丢弃,缓存自动失效并释放内存。
// 实现了"智能缓存",无需手动管理缓存过期策略。

实际案例:

  • CSS-in-JS 库 (如 styled-components, emotion):它们需要根据样式对象生成唯一的 class 名,内部大量使用 WeakMap 来缓存映射关系。
  • 响应式框架 (如 Vue 3, MobX):在建立对象属性与副作用函数(Effect)的依赖关系时,常用 WeakMap 来存储依赖收集器,确保组件销毁后依赖关系自动断开。

4. 核心场景:标记对象状态 (WeakSet)

WeakSet 常用于标记某个对象是否处于某种临时状态,而不需要关心具体的值。

场景:防止重复处理
javascript 复制代码
const processedNodes = new WeakSet();

function processNode(node) {
  if (processedNodes.has(node)) {
    return; // 已经处理过,跳过
  }
  
  // 执行耗时操作...
  doHeavyWork(node);
  
  processedNodes.add(node); // 标记为已处理
}

// 当 node 被移除出 DOM,它会自动从 processedNodes 中消失。
// 下次如果创建了一个全新的 node 对象(即使内容一样),也不会冲突。

总结:什么时候用?什么时候不用?

特性 Map / Set WeakMap / WeakSet
引用强度 强引用 (防止 GC) 弱引用 (允许 GC)
可遍历性 ✅ 可遍历 (keys, values, forEach) ❌ 不可遍历 (黑盒)
Key 类型 任意类型 必须是对象
适用场景 需要列出所有项、需要持久化缓存、配置表 私有数据、框架元数据、防循环引用、DOM 关联数据
内存风险 容易泄漏 (忘了 delete) 零泄漏 (自动清理)

四、面试高频问题汇总 (Q&A)

Q1: Map 和 Object 有什么区别?什么时候用 Map?
  • 回答要点 :
    1. 键的类型:Map 支持任意类型(对象、函数等),Object 仅限 String/Symbol。
    2. 顺序:Map 严格保持插入顺序。
    3. Size :Map 内置 size,Object 需手动统计。
    4. 性能:在频繁添加/删除键的场景下,Map 性能通常更好。
    5. 原型干扰 :Object 继承自原型链,可能有默认键(如 toString),Map 是纯净的。
  • 结论:当键不是字符串、需要频繁增删、或需要保持顺序时,优先用 Map。纯数据配置可用 Object。
Q2: Set 如何实现数组去重?原理是什么?
  • 回答 :[...new Set(arr)]。
  • 原理 :Set 内部通过类似"Same-value-zero"算法判断相等性(类似于 ===,但 NaN === NaN 为 false,而 Set 认为 NaN 等于 NaN)。添加时若已存在则忽略。
Q3: WeakMap 的键为什么必须是对象?
  • 回答 :因为弱引用的机制是针对内存地址的。原始类型(string, number)是按值存储的,不存在"引用"概念,也没有所谓的"其他地方是否还有引用"的问题,所以无法实现弱引用逻辑。只有对象才有引用计数的概念。
Q4: 既然 WeakMap 这么好,为什么不全部用它?
  • 回答 :因为它有局限性:

    1. 不可遍历:无法获取所有键或值,无法转换为 JSON。
    2. 不可预测的大小:无法知道当前有多少元素。
    3. 键必须是对象。
    • 如果需要遍历、序列化或检查大小,必须用 Map。
Q5: Map 的键如果是对象,怎么判断是否相同?
  • 回答 :Map 比较对象键时,比较的是内存地址(引用) ,而不是对象的内容。

    javascript 复制代码
    const map = new Map();
    const a = { id: 1 };
    const b = { id: 1 };
    map.set(a, 'A');
    map.get(b); // undefined (因为 a !== b,地址不同)
    map.get(a); // 'A'
相关推荐
小雪崩1 小时前
嵌入式学习 day64:字符设备驱动
学习
深圳老胡1 小时前
STM32CubeMX 生成 CMake 工程,用 VSCode 编译与调试的完整流程
笔记·stm32·单片机·代码规范
个 人 练 习 生1 小时前
C++ string 类模拟实现:从底层理解字符串(上)
开发语言·c++·经验分享·学习·程序人生
guslegend1 小时前
脚手架原理与本地调试:从 bin 软链接到 npm link
前端·npm·node.js·脚手架·前端工程化
小宋10211 小时前
A2UI从零实战:Agent安全生成交互界面与事件回传
javascript·人工智能·安全·交互
坤坤子吖1 小时前
C++智能指针:RAII、shared_ptr与内存泄漏
开发语言·c++·笔记·学习
大包子2 小时前
structuredClone vs lodash.cloneDeep:核心差异与选型指南
javascript·vue.js
海码事务所2 小时前
Google Play 新个人开发者账号上架指南:12 人连续 14 天封闭测试怎么做?
前端
乐橙开放平台2 小时前
智慧连锁客流检测和离岗检测怎么对接
大数据·人工智能·笔记·物联网·自动化·音视频·智能家居
零基础1232 小时前
FinalShell 使用教程:从安装到高效运维
运维·经验分享·笔记·finalshell