JavaScript Map Set WeakMap WeakSet

深入解析 JavaScript 集合类型:Map 与 Object 的区别、Set 的去重机制、WeakMap/WeakSet 的弱引用原理,以及在缓存和私有属性中的应用。

为什么需要 Map 和 Set

JavaScript 的 Object 虽然可以用作字典(key-value 存储),但它本质上不是为键值存储设计的——它的 key 只能是字符串或 Symbol,且有自己的原型链。MapSet 是 ES6 引入的真正为键值存储和集合操作设计的数据结构。

理解它们与 Object/Array 的区别,以及 WeakMap/WeakSet 的弱引用机制,是掌握 JavaScript 高级数据结构的必经之路。


一、Map

1.1 Map vs Object

特性 Map Object
key 类型 任意类型 字符串或 Symbol
key 顺序 保持插入顺序 基本上保持(但有例外)
大小 Map.size Object.keys(obj).length
迭代 原生可迭代 需要 Object.entries()
原型链 无(原生的干净对象) 有(可能需要 hasOwnProperty)
性能 优化用于键值存储 一般用途
// Object 作为字典的问题
const obj = {};
obj[{}] = 'value'; // {} 被转成字符串 "[object Object]"
obj[{toString: () => 'key'}] = 'value2'; // 覆盖

// Map 使用严格相等比较 key
const map = new Map();
const key1 = {};
const key2 = {};
map.set(key1, 'value');
map.set(key2, 'value2');
console.log(map.get(key1)); // 'value'
console.log(map.get(key2)); // 'value2'

1.2 Map 的基本操作

const map = new Map();

// 设置
map.set('name', 'Alice');
map.set(1, 'number key');
map.set({}, 'object key');

// 获取
console.log(map.get('name')); // 'Alice'

// 检查
console.log(map.has('name')); // true

// 删除
map.delete('name');

// 大小
console.log(map.size); // 3

// 清空
map.clear();

1.3 Map 的迭代

const map = new Map([
  ['name', 'Alice'],
  ['age', 30],
  ['city', 'NYC']
]);

// for...of
for (const [key, value] of map) {
  console.log(`${key}: ${value}`);
}

// forEach
map.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

// 解构
[...map.keys()];   // ['name', 'age', 'city']
[...map.values()]; // ['Alice', 30, 'NYC']
[...map.entries()]; // [['name', 'Alice'], ['age', 30], ['city', 'NYC']]

二、Set

2.1 Set 的特性

Set 是值的集合,不允许重复

const set = new Set();

set.add(1);
set.add(2);
set.add(1); // 重复,被忽略

console.log(set.size); // 2
console.log(set.has(1)); // true
set.delete(1);
console.log(set.has(1)); // false

2.2 Set 的去重机制

Set 使用 SameValueZero 算法判断相等,类似于 ===,但 NaN 等于自身:

const set = new Set();

set.add(NaN);
set.add(NaN); // 忽略,NaN === NaN 为 false,但 SameValueZero 认为相等
console.log(set.size); // 1

set.add({});
set.add({}); // 两个不同的空对象,不是重复
console.log(set.size); // 3

2.3 Set 的实用场景

// 数组去重
const arr = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(arr)]; // [1, 2, 3]

// 记录访问
const visited = new Set();

function visit(url) {
  if (visited.has(url)) {
    console.log('Already visited');
    return;
  }
  visited.add(url);
  console.log('First visit');
}

// 交集、并集、差集
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

// 并集
const union = new Set([...setA, ...setB]);

// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));

// 差集 (A - B)
const difference = new Set([...setA].filter(x => !setB.has(x)));

三、WeakMap

3.1 弱引用的概念

普通 Map/Set 持有对象的引用,阻止垃圾回收

const map = new Map();
const obj = {};
map.set(obj, 'value');

// 即使 obj 不再被其他代码使用
// map 仍然持有 obj 的引用,obj 不会被 GC 回收
obj = null; // map.obj 仍然存在

WeakMap 不阻止垃圾回收。当 WeakMap 的 key 对象没有任何其他引用时,该 key-value 对会被自动清理。

3.2 WeakMap 的特性

const wm = new WeakMap();

// key 必须是对象
wm.set('string', 'value'); // TypeError
wm.set({}, 'value'); // OK

// 不支持迭代
// [...wm] // TypeError
// wm.forEach // undefined

// 不支持 size
// wm.size // undefined

3.3 WeakMap 的应用

私有属性

const privateData = new WeakMap();

class User {
  constructor(name, password) {
    privateData.set(this, { name, password });
  }

  getName() {
    return privateData.get(this).name;
  }

  authenticate(password) {
    return privateData.get(this).password === password;
  }
}

// privateData 持有 User 实例的弱引用
// User 实例被 GC 后,privateData 中的对应条目自动清理

DOM 节点的元数据

const metadata = new WeakMap();

metadata.set(element, {
  lastClicked: Date.now(),
  clickCount: 0
});

function handleClick(element) {
  const data = metadata.get(element);
  data.clickCount++;
  data.lastClicked = Date.now();
}

四、WeakSet

4.1 WeakSet 的特性

WeakSet 与 Set 类似,但:

  • 元素必须是对象
  • 元素弱引用(无其他引用时可被 GC)
  • 不可迭代,不支持 size
const ws = new WeakSet();

const obj1 = {};
const obj2 = {};

ws.add(obj1);
ws.add(obj2);
ws.add('string'); // TypeError

console.log(ws.has(obj1)); // true
ws.delete(obj1);
console.log(ws.has(obj1)); // false

4.2 WeakSet 的应用

对象标记

const marked = new WeakSet();

// 标记已处理的对象
function processObject(obj) {
  if (marked.has(obj)) {
    console.log('Already processed');
    return;
  }
  // 处理逻辑...
  marked.add(obj);
}

保持对象活跃性检查

// 验证对象是否还在使用
const activeObjects = new WeakSet();

function register(obj) {
  activeObjects.add(obj);
}

function isActive(obj) {
  return activeObjects.has(obj);
}

五、Map/Set 性能对比

5.1 插入、查找、删除性能

操作 Map Object
插入 O(1)* O(1)
查找 O(1)* O(1)
删除 O(1)* O(1)

*平均情况,取决于哈希函数质量

5.2 何时使用 Map vs Object

使用 Map

  • key 是非字符串类型(对象、函数等)
  • 需要迭代或遍历
  • 需要维护插入顺序
  • key-value 操作频繁

使用 Object

  • key 是字符串或 Symbol
  • 需要原型链
  • 需要 JSON 序列化(直接)
  • 需要方法(class 的原型方法)

六、面试高频考点

考点 1:Map 和 Object 的区别

Map 的 key 可以是任意类型,使用 SameValueZero 比较;Object 的 key 只能是字符串或 Symbol。Map 原生可迭代,有 size 属性。

考点 2:WeakMap 和 Map 的区别

WeakMap 的 key 是弱引用,当 key 对象没有其他引用时会被 GC 清理。WeakMap 不可迭代,不支持 size。

考点 3:Set 的去重原理

Set 使用 SameValueZero 算法,类似于 ===,但 NaN 被视为等于自身。对象引用比较的是引用相等(同一对象),而非值相等。


延展阅读