一文搞懂ES6四大集合:Map/Set/WeakMap/WeakSet 从原理到实战
ES6引入的
Map、Set、WeakMap、WeakSet是JavaScript最重要的特性之一,彻底解决了传统对象和数组的诸多局限性。它们不仅大幅提升了代码的可读性和性能,更是现代前端开发中不可或缺的工具。本文将从底层原理→核心特性→完整API→真实应用场景→面试考点五个维度,带你彻底搞懂这四个数据结构,以及它们之间的本质区别。
一、为什么需要这四个新数据结构?
在ES6之前,JavaScript只有两种数据集合:
- 数组:有序、可重复、查找效率O(n)
- 对象:无序、键只能是字符串/Symbol、查找效率O(1)
这两种结构存在明显的缺陷:
- 数组无法保证元素唯一性,去重需要额外代码
- 对象的键只能是字符串或Symbol,无法用对象、数字等作为键
- 对象没有统一的遍历方式,也无法直接获取键值对数量
- 对象容易导致内存泄漏,因为它对所有属性都是强引用
Map、Set、WeakMap、WeakSet就是为了解决这些问题而生的:
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,这和普通的===运算符不同:javascriptconsole.log(NaN === NaN); // false const set = new Set([NaN, NaN]); console.log(set.size); // 1 ✅ 自动去重 -
Set中两个不同的对象永远是不相等的:javascriptconst 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;
}
四、核心前置知识:强引用与弱引用
在讲解WeakMap和WeakSet之前,我们必须先搞懂强引用 和弱引用的区别,这是理解这两个数据结构的唯一钥匙。
1. JavaScript的垃圾回收机制
JavaScript是自动垃圾回收的语言,垃圾回收器(GC)会定期扫描内存,自动回收那些不再被任何变量引用的对象。
GC判断一个对象是否可以被回收的唯一标准是:
这个对象是否还存在任何可达的强引用。
2. 强引用
强引用是JavaScript中默认的引用方式。只要一个对象存在强引用,GC就永远不会回收这个对象。
javascript
// obj 是对 { name: '张三' } 的强引用
let obj = { name: '张三' };
// 只要obj不被赋值为null,这个对象就永远不会被回收
强引用的优点是稳定可靠,但它也会导致内存泄漏:当我们忘记移除不再需要的强引用时,对象会永远留在内存中。
内存泄漏是指:
程序中已经不再需要的内存 ,由于某种原因没有被垃圾回收器(GC)回收,导致内存占用持续增加的现象。
简单来说就是:该释放的内存没释放,白白占用了系统资源。
3. 弱引用
弱引用是一种特殊的引用方式。如果一个对象只有弱引用指向它,GC会忽略这个引用,仍然会回收这个对象。
换句话说:
弱引用不会阻止GC回收对象。
在JavaScript中,只有WeakMap和WeakSet的键是弱引用,其他所有引用都是强引用。
4. 亲眼验证弱引用
你可以在Chrome开发者工具中亲手验证弱引用的行为:
-
打开
Memory面板 -
在控制台输入:
javascriptconst weakMap = new WeakMap(); let obj = { name: '张三' }; weakMap.set(obj, '这是一个弱引用'); console.log(weakMap.has(obj)); // true -
点击垃圾桶图标手动触发GC,再次输入
weakMap.has(obj),仍然返回true(因为还有强引用) -
输入
obj = null移除强引用 -
再次点击垃圾桶图标触发GC,输入
weakMap.has(obj),返回false(对象已经被回收)
五、WeakSet:弱引用集合
1. 核心定义
WeakSet是一种弱引用 集合,它和Set类似,但有两个关键区别:
WeakSet只能存储对象,不能存储基本类型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类似,但有两个关键区别:
WeakMap的键只能是对象,不能是基本类型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:WeakMap的值也是弱引用 错!
WeakMap只有键是弱引用,值是强引用。如果值引用了键,仍然会导致内存泄漏。 -
误区2:Weak结构可以遍历 错!
WeakSet和WeakMap都不能遍历,因为垃圾回收是不确定的。如果允许遍历,遍历结果会随时变化。 -
误区3:所有场景都应该用Map代替对象 错! 对于简单的键值对存储,普通对象更轻量、更易读。只有当你需要非字符串键、需要频繁增删、或者需要有序性时,才应该用
Map。
最佳实践
- 数组去重 :永远使用
new Set(arr) - 字典/映射表 :如果键是字符串,用普通对象;如果键是其他类型,用
Map - 给对象打标记 :永远使用
WeakSet,不要用Set - 给对象存储关联数据 :永远使用
WeakMap,不要用Map - 缓存数据 :如果缓存的键是对象,用
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的基石,掌握它们不仅能让你写出更优雅、更高效的代码,也是面试中必不可少的知识点。
希望这篇文章能帮你彻底搞懂它们,让你在实际开发中能够选择最合适的数据结构,解决各种问题。
如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,有任何问题可以在评论区留言讨论!