Map/Set/WeakMap/WeakSet

一文搞懂ES6四大集合:Map/Set/WeakMap/WeakSet 从原理到实战

ES6引入的MapSetWeakMapWeakSet是JavaScript最重要的特性之一,彻底解决了传统对象和数组的诸多局限性。它们不仅大幅提升了代码的可读性和性能,更是现代前端开发中不可或缺的工具。

本文将从底层原理→核心特性→完整API→真实应用场景→面试考点五个维度,带你彻底搞懂这四个数据结构,以及它们之间的本质区别。


一、为什么需要这四个新数据结构?

在ES6之前,JavaScript只有两种数据集合:

  • 数组:有序、可重复、查找效率O(n)
  • 对象:无序、键只能是字符串/Symbol、查找效率O(1)

这两种结构存在明显的缺陷:

  1. 数组无法保证元素唯一性,去重需要额外代码
  2. 对象的键只能是字符串或Symbol,无法用对象、数字等作为键
  3. 对象没有统一的遍历方式,也无法直接获取键值对数量
  4. 对象容易导致内存泄漏,因为它对所有属性都是强引用

MapSetWeakMapWeakSet就是为了解决这些问题而生的:

  • Set:无序不重复集合
  • Map:任意类型键的键值对集合
  • WeakSet:弱引用集合,专门用于存储对象
  • WeakMap:弱引用键的键值对集合,专门用于给对象打标记

二、Set:无序不重复集合

1. 核心定义

Set是一种无序、不重复的集合数据结构,类似于数组,但它的元素是唯一的,没有重复值。

2. 核心特性

  • ✅ 元素唯一,自动去重
  • ✅ 可以存储任何类型的值(基本类型、对象、函数等)
  • ✅ 可迭代,可以用for...of遍历
  • ✅ 插入和查找效率都是O(1),远高于数组
  • ❌ 无序(ES6规定遍历顺序等于插入顺序)
  • ❌ 没有索引,不能像数组那样通过下标访问元素

3. 完整API

javascript 复制代码
// 1. 创建Set
const set = new Set();
// 也可以从数组或可迭代对象创建
const set2 = new Set([1, 2, 3, 3, 4]); // Set(4) {1, 2, 3, 4}

// 2. 添加元素
set.add(1);
set.add('hello');
set.add({ name: '张三' });
// add方法返回Set本身,可以链式调用
set.add(2).add(3).add(4);

// 3. 删除元素
set.delete(1); // true 删除成功
set.delete(100); // false 元素不存在

// 4. 检查元素是否存在
set.has(2); // true
set.has(100); // false

// 5. 获取元素数量
console.log(set.size); // 3

// 6. 遍历
// 方法1:for...of
for (const value of set) {
  console.log(value);
}

// 方法2:forEach
set.forEach((value, index, set) => {
  console.log(value);
});

// 方法3:keys()/values()/entries()
// Set没有键,所以keys()和values()返回相同的迭代器
for (const key of set.keys()) {
  console.log(key);
}
for (const value of set.values()) {
  console.log(value);
}
for (const [key, value] of set.entries()) {
  console.log(key, value); // key和value相同
}

// 7. 清空所有元素
set.clear();
console.log(set.size); // 0

4. 最常见的应用场景

场景1:数组去重(最常用)
javascript 复制代码
const arr = [1, 2, 3, 3, 4, 4, 5];
const uniqueArr = [...new Set(arr)]; // [1, 2, 3, 4, 5]
场景2:求两个数组的交集、并集、差集
javascript 复制代码
const arr1 = [1, 2, 3, 4];
const arr2 = [3, 4, 5, 6];

const set1 = new Set(arr1);
const set2 = new Set(arr2);

// 并集
const union = [...new Set([...arr1, ...arr2])]; // [1, 2, 3, 4, 5, 6]

// 交集
const intersection = [...arr1].filter(x => set2.has(x)); // [3, 4]

// 差集(arr1有但arr2没有)
const difference = [...arr1].filter(x => !set2.has(x)); // [1, 2]
场景3:标签系统
javascript 复制代码
// 存储用户选择的标签,自动去重
const tags = new Set();
tags.add('JavaScript');
tags.add('前端');
tags.add('JavaScript'); // 不会重复添加

5. 注意事项

  • Set认为NaN等于NaN,这和普通的===运算符不同:

    javascript 复制代码
    console.log(NaN === NaN); // false
    const set = new Set([NaN, NaN]);
    console.log(set.size); // 1 ✅ 自动去重
  • Set中两个不同的对象永远是不相等的:

    javascript 复制代码
    const set = new Set([{}, {}]);
    console.log(set.size); // 2 ❌ 两个不同的对象

三、Map:任意类型键的键值对集合

1. 核心定义

Map是一种键值对 集合,类似于对象,但它的键可以是任何类型(对象、函数、数字等),而不仅仅是字符串或Symbol。

2. 核心特性

  • ✅ 键可以是任何类型的值
  • ✅ 有序(遍历顺序等于插入顺序)
  • ✅ 可迭代,可以用for...of遍历
  • ✅ 可以通过size属性直接获取键值对数量
  • ✅ 插入和查找效率都是O(1)
  • ❌ 比普通对象占用更多内存

3. 完整API

javascript 复制代码
// 1. 创建Map
const map = new Map();
// 也可以从二维数组创建
const map2 = new Map([
  ['name', '张三'],
  ['age', 20]
]);

// 2. 添加/修改键值对
map.set('name', '张三');
map.set('age', 20);
// 键可以是对象
const obj = { id: 1 };
map.set(obj, '这是一个对象作为键');
// set方法返回Map本身,可以链式调用
map.set('gender', '男').set('address', '北京市');

// 3. 获取值
console.log(map.get('name')); // '张三'
console.log(map.get(obj)); // '这是一个对象作为键'
console.log(map.get('notExist')); // undefined 键不存在返回undefined

// 4. 删除键值对
map.delete('age'); // true 删除成功
map.delete('notExist'); // false 键不存在

// 5. 检查键是否存在
map.has('name'); // true
map.has('age'); // false

// 6. 获取键值对数量
console.log(map.size); // 4

// 7. 遍历
// 方法1:for...of 遍历键值对
for (const [key, value] of map) {
  console.log(key, value);
}

// 方法2:forEach
map.forEach((value, key, map) => {
  console.log(key, value);
});

// 方法3:keys()/values()/entries()
for (const key of map.keys()) {
  console.log(key);
}
for (const value of map.values()) {
  console.log(value);
}
for (const [key, value] of map.entries()) {
  console.log(key, value);
}

// 8. 清空所有键值对
map.clear();
console.log(map.size); // 0

4. Map vs 普通对象

这是面试最常考的知识点,两者的核心区别如下:

对比维度 普通对象 Map
键的类型 只能是字符串或Symbol 可以是任何类型(对象、函数、数字等)
顺序 无序(ES6后字符串键有序,Symbol键无序) 有序(遍历顺序等于插入顺序)
大小 需要手动计算(Object.keys(obj).length 直接通过size属性获取
遍历 需要先获取键数组再遍历 原生可迭代,直接用for...of遍历
性能 频繁增删键值对时性能较差 专门为频繁增删优化,性能更好
原型链 有原型链,可能会有意外的键 没有原型链,不会有意外的键

5. 最常见的应用场景

场景1:需要用非字符串作为键的情况
javascript 复制代码
// 给DOM节点存储关联数据
const domMap = new Map();
const button = document.querySelector('button');
// 用DOM节点作为键,存储它的点击次数
domMap.set(button, { clickCount: 0 });

button.addEventListener('click', () => {
  const data = domMap.get(button);
  data.clickCount++;
  console.log(`点击了${data.clickCount}次`);
});
场景2:字典/映射表
javascript 复制代码
// 状态码映射
const statusMap = new Map([
  [200, '成功'],
  [400, '请求错误'],
  [401, '未授权'],
  [404, '资源不存在'],
  [500, '服务器错误']
]);

console.log(statusMap.get(404)); // '资源不存在'
场景3:缓存数据
javascript 复制代码
// 函数结果缓存
const cache = new Map();
function expensiveFunction(n) {
  if (cache.has(n)) {
    return cache.get(n);
  }
  // 模拟复杂计算
  const result = n * n;
  cache.set(n, result);
  return result;
}

四、核心前置知识:强引用与弱引用

在讲解WeakMapWeakSet之前,我们必须先搞懂强引用弱引用的区别,这是理解这两个数据结构的唯一钥匙。

1. JavaScript的垃圾回收机制

JavaScript是自动垃圾回收的语言,垃圾回收器(GC)会定期扫描内存,自动回收那些不再被任何变量引用的对象。

GC判断一个对象是否可以被回收的唯一标准是:

这个对象是否还存在任何可达的强引用。

2. 强引用

强引用是JavaScript中默认的引用方式。只要一个对象存在强引用,GC就永远不会回收这个对象。

javascript 复制代码
// obj 是对 { name: '张三' } 的强引用
let obj = { name: '张三' };
// 只要obj不被赋值为null,这个对象就永远不会被回收

强引用的优点是稳定可靠,但它也会导致内存泄漏:当我们忘记移除不再需要的强引用时,对象会永远留在内存中。

内存泄漏是指:

程序中已经不再需要的内存 ,由于某种原因没有被垃圾回收器(GC)回收,导致内存占用持续增加的现象。

简单来说就是:该释放的内存没释放,白白占用了系统资源。

3. 弱引用

弱引用是一种特殊的引用方式。如果一个对象只有弱引用指向它,GC会忽略这个引用,仍然会回收这个对象。

换句话说:

弱引用不会阻止GC回收对象。

在JavaScript中,只有WeakMapWeakSet的键是弱引用,其他所有引用都是强引用。

4. 亲眼验证弱引用

你可以在Chrome开发者工具中亲手验证弱引用的行为:

  1. 打开Memory面板

  2. 在控制台输入:

    javascript 复制代码
    const weakMap = new WeakMap();
    let obj = { name: '张三' };
    weakMap.set(obj, '这是一个弱引用');
    console.log(weakMap.has(obj)); // true
  3. 点击垃圾桶图标手动触发GC,再次输入weakMap.has(obj),仍然返回true(因为还有强引用)

  4. 输入obj = null移除强引用

  5. 再次点击垃圾桶图标触发GC,输入weakMap.has(obj),返回false(对象已经被回收)


五、WeakSet:弱引用集合

1. 核心定义

WeakSet是一种弱引用 集合,它和Set类似,但有两个关键区别:

  1. WeakSet只能存储对象,不能存储基本类型
  2. WeakSet对对象的引用是弱引用,如果没有其他引用指向这个对象,GC会自动回收这个对象

2. 核心特性

  • ✅ 只能存储对象
  • ✅ 弱引用,不会阻止垃圾回收,不会导致内存泄漏
  • ❌ 不可迭代,没有keys()values()entries()方法
  • ❌ 没有size属性
  • ❌ 没有clear()方法

3. 完整API

javascript 复制代码
// 1. 创建WeakSet
const weakSet = new WeakSet();
// 也可以从数组创建,但数组元素必须都是对象
const obj1 = { name: '张三' };
const obj2 = { name: '李四' };
const weakSet2 = new WeakSet([obj1, obj2]);

// 2. 添加元素
weakSet.add(obj1);
weakSet.add(obj2);

// 3. 删除元素
weakSet.delete(obj1); // true 删除成功
weakSet.delete({}); // false 不是同一个对象

// 4. 检查元素是否存在
weakSet.has(obj2); // true
weakSet.has(obj1); // false

4. 最常见的应用场景

场景1:存储DOM节点,防止内存泄漏
javascript 复制代码
// 给点击过的按钮添加标记
const clickedButtons = new WeakSet();
document.addEventListener('click', (e) => {
  if (e.target.tagName === 'BUTTON') {
    // 如果按钮已经被点击过,就不再处理
    if (clickedButtons.has(e.target)) {
      return;
    }
    // 标记为已点击
    clickedButtons.add(e.target);
    // 处理点击事件
    console.log('按钮被点击了');
  }
});

当按钮从DOM中移除时,clickedButtons中的弱引用不会阻止垃圾回收,按钮会被自动回收,不会导致内存泄漏。

场景2:给对象打标记
javascript 复制代码
// 标记哪些对象已经被处理过
const processedObjects = new WeakSet();
function processObject(obj) {
  if (processedObjects.has(obj)) {
    return;
  }
  // 处理对象
  console.log('处理对象:', obj);
  // 标记为已处理
  processedObjects.add(obj);
}

六、WeakMap:弱引用键的键值对集合

1. 核心定义

WeakMap是一种弱引用键 的键值对集合,它和Map类似,但有两个关键区别:

  1. WeakMap键只能是对象,不能是基本类型
  2. WeakMap对键的引用是弱引用,如果没有其他引用指向这个键对象,GC会自动回收这个键值对

2. 核心特性

  • ✅ 键只能是对象,值可以是任何类型
  • ✅ 键是弱引用,不会阻止垃圾回收,不会导致内存泄漏
  • ❌ 不可迭代,没有keys()values()entries()方法
  • ❌ 没有size属性
  • ❌ 没有clear()方法

3. 完整API

javascript 复制代码
// 1. 创建WeakMap
const weakMap = new WeakMap();
// 也可以从二维数组创建,但键必须都是对象
const obj1 = { id: 1 };
const obj2 = { id: 2 };
const weakMap2 = new WeakMap([
  [obj1, '张三'],
  [obj2, '李四']
]);

// 2. 添加/修改键值对
weakMap.set(obj1, { age: 20 });
weakMap.set(obj2, { age: 25 });

// 3. 获取值
console.log(weakMap.get(obj1)); // { age: 20 }
console.log(weakMap.get({})); // undefined 不是同一个对象

// 4. 删除键值对
weakMap.delete(obj1); // true 删除成功
weakMap.delete({}); // false 不是同一个对象

// 5. 检查键是否存在
weakMap.has(obj2); // true
weakMap.has(obj1); // false

4. 最常见的应用场景

场景1:实现对象的私有属性

这是WeakMap最经典的应用场景:

javascript 复制代码
// 用WeakMap存储私有属性
const privateData = new WeakMap();

class Person {
  constructor(name, age) {
    // 用this作为键,存储私有数据
    privateData.set(this, {
      name: name,
      age: age
    });
  }

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

  getAge() {
    return privateData.get(this).age;
  }
}

const p = new Person('张三', 20);
console.log(p.getName()); // '张三'
console.log(p.getAge()); // 20
// 无法直接访问私有属性
console.log(p.name); // undefined

Person实例被销毁时,privateData中的对应键值对会被自动回收,不会导致内存泄漏。

场景2:给DOM节点存储关联数据
javascript 复制代码
// 给DOM节点存储额外数据
const domData = new WeakMap();
const button = document.querySelector('button');
// 存储按钮的点击次数
domData.set(button, { clickCount: 0 });

button.addEventListener('click', () => {
  const data = domData.get(button);
  data.clickCount++;
  console.log(`点击了${data.clickCount}次`);
});

七、四者全面对比表

对比维度 Set Map WeakSet WeakMap
键的类型 无(只有值) 任何类型 无(只有值) 只能是对象
值的类型 任何类型 任何类型 只能是对象 任何类型
引用类型 强引用 强引用 弱引用 键是弱引用,值是强引用
可迭代 ✅ 是 ✅ 是 ❌ 否 ❌ 否
size属性 ✅ 有 ✅ 有 ❌ 无 ❌ 无
clear方法 ✅ 有 ✅ 有 ❌ 无 ❌ 无
垃圾回收 不会自动清理 不会自动清理 会自动清理无引用的对象 会自动清理无引用的键值对
适用场景 数组去重、集合运算 字典、映射表、缓存 给对象打标记、存储DOM节点 私有属性、存储对象关联数据

八、常见误区与最佳实践

常见误区

  1. 误区1:WeakMap的值也是弱引用 错! WeakMap只有键是弱引用,值是强引用。如果值引用了键,仍然会导致内存泄漏。

  2. 误区2:Weak结构可以遍历 错! WeakSetWeakMap都不能遍历,因为垃圾回收是不确定的。如果允许遍历,遍历结果会随时变化。

  3. 误区3:所有场景都应该用Map代替对象 错! 对于简单的键值对存储,普通对象更轻量、更易读。只有当你需要非字符串键、需要频繁增删、或者需要有序性时,才应该用Map

最佳实践

  1. 数组去重 :永远使用new Set(arr)
  2. 字典/映射表 :如果键是字符串,用普通对象;如果键是其他类型,用Map
  3. 给对象打标记 :永远使用WeakSet,不要用Set
  4. 给对象存储关联数据 :永远使用WeakMap,不要用Map
  5. 缓存数据 :如果缓存的键是对象,用WeakMap;如果是基本类型,用Map

九、面试高频考点总结

1. Set和数组的区别是什么?

  • 唯一性:Set元素自动去重,数组允许重复
  • 查找效率 :Set的has()是O(1),数组的includes()是O(n)
  • 索引:Set没有索引,无法通过下标访问元素
  • 遍历:两者都可迭代,但Set遍历顺序等于插入顺序

2. Map和普通对象的区别是什么?

  • 键的类型:Map键可以是任何类型(对象、函数、数字等),对象只能是字符串或Symbol
  • 顺序:Map严格按照插入顺序遍历,对象在ES6前无序
  • 大小 :Map通过size属性直接获取键值对数量,对象需要手动计算
  • 原型链 :Map没有原型链,不会有意外的键,对象继承自Object.prototype
  • 性能:Map在频繁增删键值对时性能优于普通对象

3. WeakSet和Set的区别是什么?

  • 元素类型:WeakSet只能存储对象,Set可以存储任何类型
  • 引用类型:WeakSet是弱引用,Set是强引用
  • 可迭代性 :WeakSet不可迭代,没有keys()values()entries()方法
  • 属性方法 :WeakSet没有size属性和clear()方法

4. WeakMap和Map的区别是什么?

  • 键的类型:WeakMap的键只能是对象,Map的键可以是任何类型
  • 引用类型:WeakMap的键是弱引用,值是强引用;Map的键和值都是强引用
  • 可迭代性 :WeakMap不可迭代,没有keys()values()entries()方法
  • 属性方法 :WeakMap没有size属性和clear()方法

5. 什么是弱引用?它有什么好处?

  • 定义:弱引用是一种不会阻止垃圾回收的引用。如果一个对象只有弱引用指向它,垃圾回收器会忽略这个引用,自动回收该对象
  • 好处:从根本上避免内存泄漏,无需手动清理不再需要的对象,尤其适合给临时对象打标记或存储关联数据

6. 如何实现对象的私有属性?

使用WeakMap实现:将类的实例作为键,私有数据作为值存储在WeakMap中。外部无法访问这个WeakMap,只能通过类的内部方法访问私有数据。当实例被销毁时,对应的私有数据会被自动回收。

javascript 复制代码
const privateData = new WeakMap();
class Person {
  constructor(name) {
    privateData.set(this, { name });
  }
  getName() {
    return privateData.get(this).name;
  }
}

7. 为什么WeakSet和WeakMap不能遍历?

因为垃圾回收的运行时机是不确定的。如果允许遍历,遍历结果会随时变化:你刚刚遍历到的元素,可能下一秒就被垃圾回收了,导致程序行为不可预测。为了保证稳定性和一致性,ECMAScript标准规定Weak结构不能被遍历。

8. 跨iframe后,instanceof Map会失效吗?为什么?

会失效

  • 每个iframe都有自己独立的全局环境,拥有自己的Map构造函数
  • 主页面的Map和iframe的Map是两个完全不同的对象,它们的prototype也不同
  • instanceof的原理是判断构造函数的prototype是否出现在对象的原型链上
  • 因此,在主页面创建的Map实例,在iframe中用instanceof Map检测会返回false

写在最后

这四个数据结构是现代JavaScript的基石,掌握它们不仅能让你写出更优雅、更高效的代码,也是面试中必不可少的知识点。

希望这篇文章能帮你彻底搞懂它们,让你在实际开发中能够选择最合适的数据结构,解决各种问题。

如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,有任何问题可以在评论区留言讨论!

相关推荐
砚底藏山河2 小时前
python、JavaScript 、JAVA,定制化数据服务,助力业务高效落地
java·javascript·python
11_x2 小时前
JS 底层:乖宝宝引擎和乖宝宝声明
javascript
flex罗小黑2 小时前
前端手机号脱敏的 4 个层级,你在第几层?
javascript
孙6903422 小时前
electron播放本地任意格式的视频
前端·javascript
openKaka_2 小时前
reconcileChildren 深入:React 如何根据 ReactElement 构建子 Fiber
前端·javascript·react.js
zithern_juejin3 小时前
typeof、instanceof与Object.prototype.toString()
javascript
Highcharts.js3 小时前
Highcharts React v5升级三问|最大的升级方向是什么?需要注意什么?有什么优化?
前端·javascript·react.js·前端框架·highcharts·大数据渲染·前端性能
129y3 小时前
JS入门参考:引擎、作用域与let/const,一起慢慢理解~
javascript
代码煮茶3 小时前
Vue3 权限系统实战 | 从 0 搭建完整 RBAC 权限管理
前端·javascript·vue.js