2025年3月7日大约 11 分钟
我将为您完善Map与Set的文档,详细介绍这两种ES6集合类型的特性、方法和应用场景。
---
title: Map与Set
icon: javascript
order: 4
---
# Map与Set
ES6引入了Map和Set两种新的集合类型。Map是键值对集合,而Set是唯一值的集合。本文将介绍这两种集合类型的特性、方法以及与普通对象和数组的区别。
## Map集合
Map是一种键值对的集合,类似于对象,但有几个重要的区别。
### Map的基本特性
1. **键的类型**:Map的键可以是任何类型(包括对象、函数、基本类型),而对象的键只能是字符串或Symbol。
2. **元素顺序**:Map中的元素按插入顺序排列。
3. **大小获取**:可以通过`size`属性直接获取Map的大小。
4. **迭代性**:Map是可迭代的,可以直接被循环。
### 创建Map
```javascript
// 创建空Map
const emptyMap = new Map();
// 使用二维数组初始化Map
const userMap = new Map([
['name', '张三'],
['age', 30],
['isAdmin', true]
]);
// 使用对象作为键
const objKey = { id: 1 };
const functionKey = function() {};
const complexMap = new Map([
[objKey, '这是一个对象键'],
[functionKey, '这是一个函数键'],
[1, '这是一个数字键']
]);
Map的方法
添加和获取元素
const userMap = new Map();
// 添加元素
userMap.set('name', '张三');
userMap.set('age', 30);
// 链式调用
userMap
.set('isAdmin', true)
.set('department', '技术部');
// 获取元素
console.log(userMap.get('name')); // '张三'
console.log(userMap.get('age')); // 30
console.log(userMap.get('notExist')); // undefined
// 检查键是否存在
console.log(userMap.has('name')); // true
console.log(userMap.has('salary')); // false
删除元素
const userMap = new Map([
['name', '张三'],
['age', 30],
['isAdmin', true]
]);
// 删除单个元素
userMap.delete('age');
console.log(userMap.has('age')); // false
// 清空Map
userMap.clear();
console.log(userMap.size); // 0
遍历Map
const userMap = new Map([
['name', '张三'],
['age', 30],
['isAdmin', true]
]);
// 遍历所有键值对
userMap.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// 使用for...of遍历
for (const [key, value] of userMap) {
console.log(`${key}: ${value}`);
}
// 只遍历键
for (const key of userMap.keys()) {
console.log(key);
}
// 只遍历值
for (const value of userMap.values()) {
console.log(value);
}
// 遍历键值对(与for...of相同)
for (const entry of userMap.entries()) {
console.log(`${entry[0]}: ${entry[1]}`);
}
Map与对象的转换
// 对象转Map
const user = {
name: '张三',
age: 30,
isAdmin: true
};
const userMap = new Map(Object.entries(user));
console.log(userMap); // Map(3) { 'name' => '张三', 'age' => 30, 'isAdmin' => true }
// Map转对象
const userObj = Object.fromEntries(userMap);
console.log(userObj); // { name: '张三', age: 30, isAdmin: true }
Map的应用场景
- 需要非字符串键:当需要使用对象、函数等作为键时。
// 使用DOM元素作为键存储数据
const elementDataMap = new Map();
const button1 = document.getElementById('button1');
const button2 = document.getElementById('button2');
elementDataMap.set(button1, { clickCount: 0, lastClicked: null });
elementDataMap.set(button2, { clickCount: 0, lastClicked: null });
button1.addEventListener('click', () => {
const data = elementDataMap.get(button1);
data.clickCount++;
data.lastClicked = new Date();
});
- 需要保持插入顺序:当元素顺序很重要时。
// 记录用户操作顺序
const userActions = new Map();
function logAction(actionId, actionData) {
userActions.set(actionId, {
...actionData,
timestamp: new Date()
});
}
logAction('login', { userId: 123 });
logAction('viewPage', { pageId: 'home' });
logAction('clickButton', { buttonId: 'signup' });
// 按照操作发生的顺序获取所有操作
console.log([...userActions.entries()]);
- 频繁添加/删除键值对:Map在频繁操作时性能更好。
// 缓存系统
const cache = new Map();
function fetchData(key, fetchFn) {
if (cache.has(key)) {
return Promise.resolve(cache.get(key));
}
return fetchFn().then(data => {
cache.set(key, data);
return data;
});
}
Set集合
Set是一种值的集合,其中的每个值都是唯一的。
Set的基本特性
- 唯一性:Set中的值不会重复。
- 值的类型:可以存储任何类型的值(包括对象、函数、基本类型)。
- 元素顺序:Set中的元素按插入顺序排列。
- 大小获取:可以通过
size
属性直接获取Set的大小。 - 迭代性:Set是可迭代的,可以直接被循环。
创建Set
// 创建空Set
const emptySet = new Set();
// 使用数组初始化Set
const fruitSet = new Set(['苹果', '香蕉', '橙子', '苹果']);
console.log(fruitSet); // Set(3) { '苹果', '香蕉', '橙子' } - 注意重复的'苹果'被去除
// 使用字符串初始化Set
const charSet = new Set('hello');
console.log(charSet); // Set(4) { 'h', 'e', 'l', 'o' } - 注意重复的'l'被去除
Set的方法
添加和检查元素
const fruitSet = new Set();
// 添加元素
fruitSet.add('苹果');
fruitSet.add('香蕉');
// 链式调用
fruitSet
.add('橙子')
.add('草莓');
// 添加重复元素(会被忽略)
fruitSet.add('苹果');
console.log(fruitSet.size); // 4,而不是5
// 检查元素是否存在
console.log(fruitSet.has('香蕉')); // true
console.log(fruitSet.has('西瓜')); // false
删除元素
const fruitSet = new Set(['苹果', '香蕉', '橙子', '草莓']);
// 删除单个元素
fruitSet.delete('香蕉');
console.log(fruitSet.has('香蕉')); // false
// 清空Set
fruitSet.clear();
console.log(fruitSet.size); // 0
遍历Set
const fruitSet = new Set(['苹果', '香蕉', '橙子', '草莓']);
// 使用forEach遍历
fruitSet.forEach(value => {
console.log(value);
});
// 使用for...of遍历
for (const fruit of fruitSet) {
console.log(fruit);
}
// 使用values()方法(与for...of相同)
for (const fruit of fruitSet.values()) {
console.log(fruit);
}
// 使用keys()方法(在Set中,keys()和values()返回相同的迭代器)
for (const fruit of fruitSet.keys()) {
console.log(fruit);
}
// 使用entries()方法(返回[value, value]对)
for (const entry of fruitSet.entries()) {
console.log(entry); // 例如: ['苹果', '苹果']
}
Set与数组的转换
// 数组转Set
const fruits = ['苹果', '香蕉', '橙子', '苹果', '香蕉'];
const fruitSet = new Set(fruits);
console.log(fruitSet); // Set(3) { '苹果', '香蕉', '橙子' }
// Set转数组
const uniqueFruits = [...fruitSet];
console.log(uniqueFruits); // ['苹果', '香蕉', '橙子']
// 或者使用Array.from()
const uniqueFruitsAlt = Array.from(fruitSet);
console.log(uniqueFruitsAlt); // ['苹果', '香蕉', '橙子']
Set的应用场景
- 去除数组重复元素:最常见的用途之一。
// 数组去重
const numbers = [1, 2, 3, 4, 2, 3, 5, 1];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // [1, 2, 3, 4, 5]
- 检查元素是否存在:比数组的
includes
方法更高效。
const blacklist = new Set(['user1', 'user2', 'user3']);
function isBlacklisted(username) {
return blacklist.has(username);
}
console.log(isBlacklisted('user2')); // true
console.log(isBlacklisted('user4')); // false
- 实现集合操作:如并集、交集、差集。
// 集合操作
const set1 = new Set([1, 2, 3, 4]);
const set2 = new Set([3, 4, 5, 6]);
// 并集
const union = new Set([...set1, ...set2]);
console.log([...union]); // [1, 2, 3, 4, 5, 6]
// 交集
const intersection = new Set([...set1].filter(x => set2.has(x)));
console.log([...intersection]); // [3, 4]
// 差集(set1中有但set2中没有的元素)
const difference = new Set([...set1].filter(x => !set2.has(x)));
console.log([...difference]); // [1, 2]
WeakMap和WeakSet
ES6还引入了WeakMap和WeakSet,它们是Map和Set的"弱引用"版本。
WeakMap
WeakMap与Map类似,但有几个重要区别:
- 键的类型限制:WeakMap的键只能是对象,不能是基本类型。
- 弱引用:WeakMap对键是弱引用,不会阻止垃圾回收。
- 不可迭代:WeakMap不可迭代,没有
keys()
、values()
、entries()
方法,也没有size
属性。 - 有限的方法:只有
get()
、set()
、has()
、delete()
方法。
// 创建WeakMap
const weakMap = new WeakMap();
// 使用对象作为键
let obj1 = { id: 1 };
let obj2 = { id: 2 };
weakMap.set(obj1, '对象1的数据');
weakMap.set(obj2, '对象2的数据');
console.log(weakMap.get(obj1)); // '对象1的数据'
// 当对象不再被引用时,WeakMap中的相应条目会被自动垃圾回收
obj1 = null; // 现在obj1指向的对象可能会被垃圾回收
// weakMap中与obj1相关的条目也会被回收
WeakMap的应用场景
- 关联数据存储:将数据关联到对象,但不影响对象的生命周期。
// 存储DOM元素的额外数据
const elementData = new WeakMap();
function setupElement(element, data) {
elementData.set(element, data);
}
const button = document.getElementById('myButton');
setupElement(button, { clickCount: 0 });
button.addEventListener('click', () => {
const data = elementData.get(button);
data.clickCount++;
console.log(`按钮被点击了${data.clickCount}次`);
});
// 当button元素被移除时,相关数据会被自动垃圾回收
- 缓存计算结果:缓存与对象相关的计算结果。
// 缓存对象的计算结果
const cache = new WeakMap();
function processObject(obj) {
if (cache.has(obj)) {
console.log('使用缓存结果');
return cache.get(obj);
}
console.log('计算新结果');
const result = expensiveComputation(obj);
cache.set(obj, result);
return result;
}
function expensiveComputation(obj) {
// 假设这是一个耗时的计算
return Object.keys(obj).length;
}
WeakSet
WeakSet与Set类似,但有几个重要区别:
- 值的类型限制:WeakSet只能存储对象,不能存储基本类型。
- 弱引用:WeakSet对值是弱引用,不会阻止垃圾回收。
- 不可迭代:WeakSet不可迭代,没有
keys()
、values()
、entries()
方法,也没有size
属性。 - 有限的方法:只有
add()
、has()
、delete()
方法。
// 创建WeakSet
const weakSet = new WeakSet();
// 添加对象
let obj1 = { id: 1 };
let obj2 = { id: 2 };
weakSet.add(obj1);
weakSet.add(obj2);
console.log(weakSet.has(obj1)); // true
// 当对象不再被引用时,WeakSet中的相应条目会被自动垃圾回收
obj1 = null; // 现在obj1指向的对象可能会被垃圾回收
// weakSet中与obj1相关的条目也会被回收
WeakSet的应用场景
- 标记对象:用于标记对象而不影响垃圾回收。
// 使用WeakSet标记已处理的对象
const processedObjects = new WeakSet();
function processObject(obj) {
if (processedObjects.has(obj)) {
console.log('对象已处理,跳过');
return;
}
// 处理对象
console.log('处理对象:', obj);
// 标记为已处理
processedObjects.add(obj);
}
const obj = { data: 'some data' };
processObject(obj); // 处理对象
processObject(obj); // 对象已处理,跳过
- 防止循环引用:在递归算法中防止重复处理同一对象。
// 防止循环引用导致的无限递归
function traverseObject(obj, visited = new WeakSet()) {
// 检查对象是否已访问过
if (visited.has(obj)) {
console.log('检测到循环引用,跳过');
return;
}
// 标记对象为已访问
visited.add(obj);
// 处理对象的属性
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
console.log(`递归处理属性: ${key}`);
traverseObject(obj[key], visited);
}
}
}
// 创建带有循环引用的对象
const obj1 = { name: '对象1' };
const obj2 = { name: '对象2', ref: obj1 };
obj1.ref = obj2; // 创建循环引用
traverseObject(obj1); // 不会导致无限递归
Map、Set与传统集合的比较
Map vs 对象
特性 | Map | 对象 |
---|---|---|
键类型 | 任何值 | 字符串或Symbol |
元素顺序 | 按插入顺序 | 无固定顺序(ES2015后有部分顺序保证) |
大小获取 | size 属性 | 需要手动计算(如Object.keys(obj).length ) |
迭代性 | 直接可迭代 | 需要使用Object.keys() 等方法 |
性能 | 频繁增删更优 | 简单使用场景更优 |
Set vs 数组
特性 | Set | 数组 |
---|---|---|
值唯一性 | 自动去重 | 可包含重复值 |
查找性能 | O(1) | O(n) |
元素顺序 | 按插入顺序 | 按索引顺序 |
索引访问 | 不支持 | 支持 |
常用操作 | 添加/删除/检查 | 添加/删除/修改/遍历 |
实际应用示例
使用Map实现LRU缓存
LRU(Least Recently Used,最近最少使用)缓存是一种常见的缓存策略,它会在缓存满时删除最久未使用的项目。
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
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);
}
// 如果缓存已满,删除最久未使用的项(第一个)
else if (this.cache.size >= this.capacity) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
// 添加新项到末尾
this.cache.set(key, value);
}
}
// 使用示例
const cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
console.log(cache.get(1)); // 返回 1
cache.put(3, 3); // 删除 key 2
console.log(cache.get(2)); // 返回 -1 (未找到)
cache.put(4, 4); // 删除 key 1
console.log(cache.get(1)); // 返回 -1 (未找到)
console.log(cache.get(3)); // 返回 3
console.log(cache.get(4)); // 返回 4
使用Set实现并查集
并查集是一种树形数据结构,用于处理一些不交集的合并及查询问题。
class DisjointSet {
constructor() {
// 使用Map存储节点的父节点
this.parent = new Map();
// 使用Set存储所有集合
this.sets = new Set();
}
// 创建一个新集合
makeSet(x) {
if (!this.parent.has(x)) {
this.parent.set(x, x);
this.sets.add(x);
}
}
// 查找元素所属的集合(路径压缩)
find(x) {
if (!this.parent.has(x)) return null;
if (this.parent.get(x) !== x) {
this.parent.set(x, this.find(this.parent.get(x)));
}
return this.parent.get(x);
}
// 合并两个集合
union(x, y) {
const rootX = this.find(x);
const rootY = this.find(y);
if (rootX === null || rootY === null) return false;
if (rootX === rootY) return true;
this.parent.set(rootY, rootX);
this.sets.delete(rootY);
return true;
}
// 检查两个元素是否属于同一集合
connected(x, y) {
return this.find(x) === this.find(y);
}
// 获取集合数量
count() {
return this.sets.size;
}
}
// 使用示例
const ds = new DisjointSet();
ds.makeSet('A');
ds.makeSet('B');
ds.makeSet('C');
ds.makeSet('D');
ds.union('A', 'B');
ds.union('C', 'D');
console.log(ds.connected('A', 'B')); // true
console.log(ds.connected('A', 'C')); // false
ds.union('B', 'C');
console.log(ds.connected('A', 'C')); // true
console.log(ds.count()); // 1
性能考虑
在选择使用Map/Set还是传统对象/数组时,应考虑以下性能因素:
查找性能:
- 在大型集合中查找元素时,Map和Set(O(1))比对象和数组(O(n))更高效。
内存使用:
- Map和Set通常比简单对象和数组消耗更多内存。
- WeakMap和WeakSet在处理大量对象引用时可以减少内存泄漏风险。
操作频率:
- 如果频繁添加和删除元素,Map和Set更合适。
- 如果主要是读取操作,简单对象可能更高效。
键的类型:
- 如果需要非字符串键,必须使用Map。
- 如果只需要字符串键,对象可能更简单高效。
总结
ES6引入的Map和Set集合类型为JavaScript提供了更强大、更灵活的数据结构选择:
- Map是键值对集合,支持任何类型的键,并保持插入顺序。
- Set是唯一值的集合,自动去除重复项,并保持插入顺序。
- WeakMap和WeakSet是它们的"弱引用"版本,适用于需要避免内存泄漏的场景。
这些集合类型与传统的对象和数组相比,各有优缺点:
- 在需要非字符串键、保持插入顺序或频繁添加/删除元素时,Map和Set更合适。
- 在简单场景或需要JSON序列化时,传统对象和数组可能更方便。
通过合理选择和使用这些集合类型,可以编写更高效、更清晰的代码,解决各种复杂的数据处理问题。