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

相关推荐
李可以量化1 小时前
成交量的终极量化策略:价量共振指标完整实现(下篇)
前端·数据库·人工智能
copyer_xyf2 小时前
Python 如何同时做很多事:进程、线程、协程
前端·后端·python
gqk012 小时前
Delegate.Target/ Method
前端·ui·xhtml
有梦想的程序星空3 小时前
【环境配置】Vue3项目离线化本地部署echarts全攻略
前端·javascript·vue·echarts
IT_陈寒3 小时前
被Vite的动态导入坑了一整天,原来问题出在这
前端·人工智能·后端
薛先生_0993 小时前
vue-路由重定向
前端·javascript·vue.js
橘子星4 小时前
基于 ES6 语法的 NLP 任务模块化开发实践
前端·javascript
玉宇夕落4 小时前
Props的传递学习
前端
月光刺眼4 小时前
JS 底层执行机制探讨:执行上下文、变量提升与调用栈
前端·javascript