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()

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

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

以下是它们的具体用途:

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: MapObject 有什么区别?什么时候用 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'
相关推荐
峥嵘life1 小时前
Android16 【GTS】 GtsDevicePolicyTestCases 测试存在Failed项
android·linux·学习
Luna-player1 小时前
前端中stylus是干嘛用的
前端·css·stylus
菩提小狗1 小时前
第23天:安全开发-PHP应用&后台模块&Session&Cookie&Toke_笔记|小迪安全2023-2024|web安全|渗透测试|
笔记·安全·php
CHQIUU2 小时前
解决 npm 全局安装 EACCES 权限问题(macOS 篇)
前端·macos·npm
程序员鱼皮2 小时前
OpenClaw接入飞书保姆级教程,几分钟搞定手机养龙虾!
前端·人工智能·后端
HuDie3402 小时前
黑马多模态AIGC课程笔记
笔记·aigc
leixj0252 小时前
SVN学习笔记
笔记·学习·svn
紫_龙2 小时前
最新版vue3+TypeScript开发入门到实战教程之vue3与vue2语法优劣对比
前端·javascript·typescript
毕设源码_廖学姐2 小时前
计算机毕业设计springboot古诗词学习App 基于SpringBoot的中华经典诗文数字化研习平台 SpringBoot框架下的传统诗词文化移动学习系统
spring boot·学习·课程设计