在 JavaScript 的世界里,数组(Array)和对象(Object)一直是我们处理数据的主力军。但随着 ES6 的到来,两个新的数据结构悄然登场:Set 和 Map。它们不仅解决了传统数据结构的一些痛点,还为我们提供了更优雅、更高效的解决方案。
今天,我们就来深入探讨这两个现代 JavaScript 的数据结构明星。
什么是 Set?
Set 就像是一个"绝不允许重复"的数组。它是一个集合,存储唯一值的集合,任何重复的值都会被自动忽略。
javascript
// 传统数组允许重复
const numbers = [1, 2, 2, 3, 3, 3];
console.log(numbers); // [1, 2, 2, 3, 3, 3]
// Set 自动去重
const uniqueNumbers = new Set([1, 2, 2, 3, 3, 3]);
console.log(uniqueNumbers); // Set(3) {1, 2, 3}
什么是 Map?
Map 就像是一个"超级对象"。与普通对象只能使用字符串(和 Symbol)作为键不同,Map 可以使用任何类型的值作为键,包括对象、函数,甚至是其他 Map。
javascript
// 传统对象的限制
const obj = {};
obj[1] = 'number key';
obj['1'] = 'string key';
console.log(obj); // {'1': 'string key'} - 数字键被转换为字符串!
// Map 的灵活性
const map = new Map();
map.set(1, 'number key');
map.set('1', 'string key');
map.set(true, 'boolean key');
console.log(map.get(1)); // 'number key'
console.log(map.get('1')); // 'string key'
console.log(map.get(true)); // 'boolean key'
Set 的深入探索
基础操作
javascript
// 创建 Set
const mySet = new Set();
// 添加元素
mySet.add(1);
mySet.add(2);
mySet.add(2); // 重复值会被忽略
mySet.add('hello');
console.log(mySet); // Set(3) {1, 2, 'hello'}
console.log(mySet.size); // 3
// 检查元素是否存在
console.log(mySet.has(1)); // true
console.log(mySet.has('hi')); // false
// 删除元素
mySet.delete(2);
console.log(mySet); // Set(2) {1, 'hello'}
// 清空 Set
mySet.clear();
console.log(mySet); // Set(0) {}
实战场景1:数组去重
javascript
// 传统方法:使用 filter + indexOf
function removeDuplicatesOld(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
const numbers = [1, 2, 2, 3, 4, 4, 5];
console.time('传统方法');
const result1 = removeDuplicatesOld(numbers);
console.timeEnd('传统方法');
console.log(result1); // [1, 2, 3, 4, 5]
// Set 方法:简洁高效
console.time('Set 方法');
const result2 = [...new Set(numbers)];
console.timeEnd('Set 方法');
console.log(result2); // [1, 2, 3, 4, 5]
// 处理复杂数据
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' },
{ id: 3, name: 'Charlie' }
];
// Set 对对象的处理(注意:对象引用比较)
const userSet = new Set(users);
console.log(userSet.size); // 4 - 因为是不同的对象引用
// 基于属性去重的正确方法
function uniqueById(arr) {
const seen = new Set();
return arr.filter(user => {
if (seen.has(user.id)) {
return false;
}
seen.add(user.id);
return true;
});
}
console.log(uniqueById(users)); // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]
实战场景2:集合运算
javascript
// 使用 Set 实现数学集合运算
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// 并集 (Union)
const union = new Set([...setA, ...setB]);
console.log('并集:', [...union]); // [1, 2, 3, 4, 5, 6]
// 交集 (Intersection)
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log('交集:', [...intersection]); // [3, 4]
// 差集 (Difference)
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log('差集:', [...difference]); // [1, 2]
// 封装为工具函数
class SetOperations {
static union(setA, setB) {
return new Set([...setA, ...setB]);
}
static intersection(setA, setB) {
return new Set([...setA].filter(x => setB.has(x)));
}
static difference(setA, setB) {
return new Set([...setA].filter(x => !setB.has(x)));
}
static symmetricDifference(setA, setB) {
return new Set([
...[...setA].filter(x => !setB.has(x)),
...[...setB].filter(x => !setA.has(x))
]);
}
}
// 实际应用:用户权限管理
const adminPermissions = new Set(['read', 'write', 'delete', 'admin']);
const userPermissions = new Set(['read', 'write']);
const guestPermissions = new Set(['read']);
console.log('管理员独有权限:',
[...SetOperations.difference(adminPermissions, userPermissions)]
); // ['delete', 'admin']
console.log('用户和访客共同权限:',
[...SetOperations.intersection(userPermissions, guestPermissions)]
); // ['read']
实战场景3:性能优化的查找
javascript
// 场景:检查大量数据中是否存在某个值
const largeArray = Array.from({length: 100000}, (_, i) => i);
const targetValues = [50000, 75000, 99999, 100001];
// 传统方法:使用 includes
console.time('Array includes');
targetValues.forEach(target => {
const found = largeArray.includes(target);
// console.log(`${target}: ${found}`);
});
console.timeEnd('Array includes'); // 较慢
// Set 方法:O(1) 查找时间
const largeSet = new Set(largeArray);
console.time('Set has');
targetValues.forEach(target => {
const found = largeSet.has(target);
// console.log(`${target}: ${found}`);
});
console.timeEnd('Set has'); // 更快
// 实际应用:黑名单检查
class BlacklistChecker {
constructor(blacklist) {
this.blacklistSet = new Set(blacklist);
}
isBlocked(item) {
return this.blacklistSet.has(item);
}
addToBlacklist(item) {
this.blacklistSet.add(item);
}
removeFromBlacklist(item) {
this.blacklistSet.delete(item);
}
}
const checker = new BlacklistChecker(['spam@email.com', 'bad-user', '192.168.1.100']);
console.log(checker.isBlocked('spam@email.com')); // true
console.log(checker.isBlocked('good@email.com')); // false
Map 的深入探索
基础操作
javascript
// 创建 Map
const myMap = new Map();
// 设置键值对
myMap.set('name', 'Alice');
myMap.set(42, 'number key');
myMap.set(true, 'boolean key');
// 链式调用
myMap.set('age', 25).set('city', 'New York');
console.log(myMap.size); // 5
// 获取值
console.log(myMap.get('name')); // 'Alice'
console.log(myMap.get(42)); // 'number key'
console.log(myMap.get('missing')); // undefined
// 检查键是否存在
console.log(myMap.has('name')); // true
console.log(myMap.has('email')); // false
// 删除键值对
myMap.delete(42);
console.log(myMap.has(42)); // false
// 清空 Map
// myMap.clear();
Map vs Object:关键区别
javascript
// 1. 键的类型限制
const obj = {};
const map = new Map();
// Object:键会被转换为字符串
obj[1] = 'one';
obj['1'] = 'string one';
console.log(obj); // {'1': 'string one'} - 数字键被覆盖了!
// Map:保持键的原始类型
map.set(1, 'one');
map.set('1', 'string one');
console.log(map.get(1)); // 'one'
console.log(map.get('1')); // 'string one'
// 2. 大小获取
console.log('Object size:', Object.keys(obj).length); // 需要计算
console.log('Map size:', map.size); // 直接属性
// 3. 迭代方式
const dataObj = { a: 1, b: 2, c: 3 };
const dataMap = new Map([['a', 1], ['b', 2], ['c', 3]]);
// Object 迭代
console.log('Object keys:', Object.keys(dataObj));
console.log('Object values:', Object.values(dataObj));
console.log('Object entries:', Object.entries(dataObj));
// Map 迭代(更直接)
console.log('Map keys:', [...dataMap.keys()]);
console.log('Map values:', [...dataMap.values()]);
console.log('Map entries:', [...dataMap.entries()]);
// 4. 性能对比
console.time('Object creation');
const objTest = {};
for (let i = 0; i < 100000; i++) {
objTest[i] = i;
}
console.timeEnd('Object creation');
console.time('Map creation');
const mapTest = new Map();
for (let i = 0; i < 100000; i++) {
mapTest.set(i, i);
}
console.timeEnd('Map creation');
实战场景1:缓存系统
javascript
// 传统对象缓存的问题
class OldCache {
constructor() {
this.cache = {};
}
set(key, value) {
// 问题:所有键都会被转换为字符串
this.cache[key] = value;
}
get(key) {
return this.cache[key];
}
has(key) {
return key in this.cache;
}
clear() {
this.cache = {};
}
}
// Map 实现的现代缓存
class ModernCache {
constructor(maxSize = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
set(key, value) {
// 如果缓存已满,删除最旧的条目
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
value,
timestamp: Date.now()
});
}
get(key) {
const item = this.cache.get(key);
if (item) {
// 更新访问时间(LRU 策略)
this.cache.delete(key);
this.cache.set(key, {
...item,
lastAccessed: Date.now()
});
return item.value;
}
return undefined;
}
has(key) {
return this.cache.has(key);
}
delete(key) {
return this.cache.delete(key);
}
clear() {
this.cache.clear();
}
get size() {
return this.cache.size;
}
// 清理过期缓存
cleanup(maxAge = 60000) { // 默认 1 分钟
const now = Date.now();
for (const [key, item] of this.cache) {
if (now - item.timestamp > maxAge) {
this.cache.delete(key);
}
}
}
}
// 使用示例
const cache = new ModernCache(3);
// 可以使用任何类型作为键
const userObj = { id: 1 };
const apiEndpoint = '/api/users';
cache.set(userObj, { name: 'Alice', age: 25 });
cache.set(apiEndpoint, { data: [1, 2, 3] });
cache.set(42, 'number key');
cache.set('string-key', 'string value');
console.log(cache.get(userObj)); // { name: 'Alice', age: 25 }
console.log(cache.size); // 3 (最大容量限制)
// 测试过期清理
setTimeout(() => {
cache.cleanup(1000); // 清理 1 秒前的缓存
}, 2000);
实战场景2:DOM 元素关联数据
javascript
// 传统方法:在 DOM 元素上添加属性
function oldWay() {
const button = document.createElement('button');
button.textContent = 'Click me';
// 直接在 DOM 元素上添加自定义属性(不推荐)
button._customData = {
clickCount: 0,
lastClicked: null
};
button.addEventListener('click', function() {
this._customData.clickCount++;
this._customData.lastClicked = new Date();
console.log('Clicked', this._customData.clickCount, 'times');
});
return button;
}
// Map 方法:清洁的数据关联
class DOMDataManager {
constructor() {
this.elementData = new Map();
}
setData(element, data) {
this.elementData.set(element, data);
}
getData(element) {
return this.elementData.get(element);
}
updateData(element, updates) {
const currentData = this.getData(element) || {};
this.setData(element, { ...currentData, ...updates });
}
removeData(element) {
this.elementData.delete(element);
}
// 清理已从 DOM 中移除的元素数据
cleanup() {
for (const [element] of this.elementData) {
if (!document.contains(element)) {
this.elementData.delete(element);
}
}
}
}
// 使用示例
const dataManager = new DOMDataManager();
function createSmartButton(text) {
const button = document.createElement('button');
button.textContent = text;
// 使用 Map 存储元素相关数据
dataManager.setData(button, {
clickCount: 0,
lastClicked: null,
created: new Date()
});
button.addEventListener('click', function() {
const data = dataManager.getData(this);
dataManager.updateData(this, {
clickCount: data.clickCount + 1,
lastClicked: new Date()
});
const updatedData = dataManager.getData(this);
console.log(`Button clicked ${updatedData.clickCount} times`);
});
return button;
}
// 创建多个按钮
const button1 = createSmartButton('Button 1');
const button2 = createSmartButton('Button 2');
// 每个按钮都有独立的数据,且不污染 DOM
document.body.appendChild(button1);
document.body.appendChild(button2);
实战场景3:对象关系映射
javascript
// 使用 Map 建立对象之间的关系
class RelationshipManager {
constructor() {
this.relationships = new Map();
}
// 建立关系
relate(object1, object2, relationshipType) {
if (!this.relationships.has(object1)) {
this.relationships.set(object1, new Map());
}
this.relationships.get(object1).set(object2, relationshipType);
}
// 获取关系
getRelationship(object1, object2) {
const relations = this.relationships.get(object1);
return relations ? relations.get(object2) : undefined;
}
// 获取所有相关对象
getRelatedObjects(object, relationshipType = null) {
const relations = this.relationships.get(object);
if (!relations) return [];
const result = [];
for (const [relatedObject, type] of relations) {
if (!relationshipType || type === relationshipType) {
result.push({ object: relatedObject, type });
}
}
return result;
}
// 移除关系
removeRelationship(object1, object2) {
const relations = this.relationships.get(object1);
if (relations) {
relations.delete(object2);
if (relations.size === 0) {
this.relationships.delete(object1);
}
}
}
}
// 实际应用:用户关系系统
const relationManager = new RelationshipManager();
const alice = { name: 'Alice', id: 1 };
const bob = { name: 'Bob', id: 2 };
const charlie = { name: 'Charlie', id: 3 };
const diana = { name: 'Diana', id: 4 };
// 建立关系
relationManager.relate(alice, bob, 'friend');
relationManager.relate(alice, charlie, 'colleague');
relationManager.relate(alice, diana, 'family');
relationManager.relate(bob, charlie, 'friend');
// 查询关系
console.log('Alice 和 Bob 的关系:',
relationManager.getRelationship(alice, bob)); // 'friend'
console.log('Alice 的所有朋友:',
relationManager.getRelatedObjects(alice, 'friend')); // [{ object: bob, type: 'friend' }]
console.log('Alice 的所有关系:',
relationManager.getRelatedObjects(alice));
进阶特性和技巧
1. WeakSet 和 WeakMap
javascript
// WeakSet:弱引用的 Set
const weakSet = new WeakSet();
let obj1 = { name: 'Alice' };
let obj2 = { name: 'Bob' };
weakSet.add(obj1);
weakSet.add(obj2);
console.log(weakSet.has(obj1)); // true
// 当对象被垃圾回收时,WeakSet 中的引用也会自动清除
obj1 = null; // 现在 obj1 可能被垃圾回收
// WeakMap:弱引用的 Map
const weakMap = new WeakMap();
let element = document.createElement('div');
// 存储 DOM 元素的私有数据
weakMap.set(element, {
clickCount: 0,
initialized: Date.now()
});
console.log(weakMap.get(element)); // { clickCount: 0, initialized: ... }
// 当 DOM 元素被移除时,相关数据也会被自动清理
element = null;
// 实际应用:私有数据存储
const privateData = new WeakMap();
class SecureUser {
constructor(name, password) {
this.name = name;
// 使用 WeakMap 存储私有数据
privateData.set(this, {
password: password,
loginAttempts: 0,
lastLogin: null
});
}
authenticate(password) {
const data = privateData.get(this);
if (data.password === password) {
data.lastLogin = new Date();
data.loginAttempts = 0;
return true;
} else {
data.loginAttempts++;
return false;
}
}
getLoginInfo() {
const data = privateData.get(this);
return {
lastLogin: data.lastLogin,
loginAttempts: data.loginAttempts
};
}
}
const user = new SecureUser('Alice', 'secret123');
console.log(user.authenticate('wrong')); // false
console.log(user.authenticate('secret123')); // true
console.log(user.getLoginInfo()); // { lastLogin: ..., loginAttempts: 0 }
// 无法直接访问密码
console.log(user.password); // undefined
2. 迭代和遍历
javascript
// Set 的迭代方法
const mySet = new Set(['a', 'b', 'c']);
// 1. for...of 循环
for (const value of mySet) {
console.log(value); // 'a', 'b', 'c'
}
// 2. forEach 方法
mySet.forEach((value, valueAgain, set) => {
console.log(value); // 注意:Set 的 forEach 中,value 和 valueAgain 是相同的
});
// 3. 迭代器方法
console.log([...mySet.keys()]); // ['a', 'b', 'c']
console.log([...mySet.values()]); // ['a', 'b', 'c']
console.log([...mySet.entries()]); // [['a', 'a'], ['b', 'b'], ['c', 'c']]
// Map 的迭代方法
const myMap = new Map([
['name', 'Alice'],
['age', 25],
['city', 'New York']
]);
// 1. for...of 循环
for (const [key, value] of myMap) {
console.log(`${key}: ${value}`);
}
// 2. forEach 方法
myMap.forEach((value, key, map) => {
console.log(`${key} => ${value}`);
});
// 3. 分别迭代键和值
for (const key of myMap.keys()) {
console.log('Key:', key);
}
for (const value of myMap.values()) {
console.log('Value:', value);
}
// 4. 迭代条目
for (const [key, value] of myMap.entries()) {
console.log(`Entry: ${key} = ${value}`);
}
// 实际应用:数据转换
function mapToObject(map) {
const obj = {};
for (const [key, value] of map) {
obj[key] = value;
}
return obj;
}
function objectToMap(obj) {
return new Map(Object.entries(obj));
}
const originalMap = new Map([['a', 1], ['b', 2]]);
const obj = mapToObject(originalMap);
const newMap = objectToMap(obj);
console.log(obj); // { a: 1, b: 2 }
console.log(newMap); // Map(2) { 'a' => 1, 'b' => 2 }
3. 高级应用:实现 LRU 缓存
javascript
// 使用 Map 实现 LRU (Least Recently Used) 缓存
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (this.cache.has(key)) {
// 获取值并重新插入到末尾(表示最近使用)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return -1;
}
put(key, value) {
if (this.cache.has(key)) {
// 如果键已存在,删除旧的
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// 如果缓存已满,删除最久未使用的(第一个)
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
// 添加新的键值对到末尾
this.cache.set(key, value);
}
// 获取缓存状态
getState() {
return Array.from(this.cache.entries());
}
}
// 测试 LRU 缓存
const lru = new LRUCache(3);
lru.put(1, 'one');
lru.put(2, 'two');
lru.put(3, 'three');
console.log('初始状态:', lru.getState()); // [[1, 'one'], [2, 'two'], [3, 'three']]
lru.get(1); // 访问 1,将其移到最后
console.log('访问 1 后:', lru.getState()); // [[2, 'two'], [3, 'three'], [1, 'one']]
lru.put(4, 'four'); // 添加新元素,删除最久未使用的 2
console.log('添加 4 后:', lru.getState()); // [[3, 'three'], [1, 'one'], [4, 'four']]
4. 性能优化和最佳实践
javascript
// 性能比较:Set vs Array 查找
function performanceTest() {
const size = 100000;
const array = Array.from({length: size}, (_, i) => i);
const set = new Set(array);
const searchValues = [0, size/4, size/2, size*3/4, size-1];
console.log('=== 查找性能测试 ===');
// Array.includes() 测试
console.time('Array includes');
searchValues.forEach(val => array.includes(val));
console.timeEnd('Array includes');
// Set.has() 测试
console.time('Set has');
searchValues.forEach(val => set.has(val));
console.timeEnd('Set has');
console.log('=== 添加性能测试 ===');
// Array push 测试
const testArray = [];
console.time('Array push');
for (let i = 0; i < 10000; i++) {
testArray.push(i);
}
console.timeEnd('Array push');
// Set add 测试
const testSet = new Set();
console.time('Set add');
for (let i = 0; i < 10000; i++) {
testSet.add(i);
}
console.timeEnd('Set add');
}
// performanceTest();
// 最佳实践示例
class DataProcessor {
constructor() {
this.processedIds = new Set(); // 使用 Set 跟踪已处理的 ID
this.cache = new Map(); // 使用 Map 作为缓存
this.config = new Map([ // 使用 Map 存储配置
['maxRetries', 3],
['timeout', 5000],
['batchSize', 100]
]);
}
async processData(items) {
const results = [];
const batch = [];
for (const item of items) {
// 使用 Set 快速检查是否已处理
if (this.processedIds.has(item.id)) {
console.log(`Skipping already processed item: ${item.id}`);
continue;
}
// 使用 Map 检查缓存
if (this.cache.has(item.id)) {
results.push(this.cache.get(item.id));
continue;
}
batch.push(item);
// 批量处理
if (batch.length >= this.config.get('batchSize')) {
const batchResults = await this.processBatch(batch);
results.push(...batchResults);
batch.length = 0; // 清空批次
}
}
// 处理剩余项目
if (batch.length > 0) {
const batchResults = await this.processBatch(batch);
results.push(...batchResults);
}
return results;
}
async processBatch(items) {
const results = [];
for (const item of items) {
try {
const result = await this.processItem(item);
// 标记为已处理
this.processedIds.add(item.id);
// 缓存结果
this.cache.set(item.id, result);
results.push(result);
} catch (error) {
console.error(`Error processing item ${item.id}:`, error);
}
}
return results;
}
async processItem(item) {
// 模拟异步处理
return new Promise(resolve => {
setTimeout(() => {
resolve({
id: item.id,
processed: true,
timestamp: Date.now(),
data: item.data?.toUpperCase() || 'NO DATA'
});
}, 10);
});
}
// 清理方法
cleanup() {
this.processedIds.clear();
this.cache.clear();
}
// 获取统计信息
getStats() {
return {
processedCount: this.processedIds.size,
cacheSize: this.cache.size,
config: Object.fromEntries(this.config)
};
}
}
// 使用示例
async function demonstrateProcessor() {
const processor = new DataProcessor();
const testData = [
{ id: 1, data: 'hello' },
{ id: 2, data: 'world' },
{ id: 1, data: 'hello' }, // 重复 ID
{ id: 3, data: 'test' }
];
const results = await processor.processData(testData);
console.log('处理结果:', results);
console.log('统计信息:', processor.getStats());
}
// demonstrateProcessor();
何时使用 Set 和 Map?
使用 Set 的场景:
- 需要存储唯一值:去重、标签系统、权限管理
- 需要高效查找:黑名单检查、已访问页面跟踪
- 集合运算:交集、并集、差集操作
- 性能敏感的查找操作:大数据量的存在性检查
使用 Map 的场景:
- 需要非字符串键:对象作为键、数字键与字符串键区分
- 频繁添加删除键值对:动态配置、缓存系统
- 需要保持插入顺序:有序的键值对存储
- 需要获取大小:经常需要知道键值对数量
避免使用的场景:
- Set:需要索引访问、需要存储重复值、需要复杂的数据结构
- Map:只需要简单的字符串键值对、需要 JSON 序列化、需要原型继承
总结
Set 和 Map 是现代 JavaScript 中强大的数据结构,它们不仅解决了传统数组和对象的一些限制,还提供了更好的性能和更清晰的语义。
Set 的核心优势:
- 自动去重
- 高效的查找性能(O(1))
- 清晰的集合语义
- 丰富的迭代方法
Map 的核心优势:
- 任意类型的键
- 保持插入顺序
- 高效的键值对操作
- 清晰的大小管理
掌握这两个数据结构,能让你写出更高效、更清晰的 JavaScript 代码。在合适的场景下使用它们,不仅能提升性能,还能让代码的意图更加明确。
记住:选择合适的数据结构是编程的基本功。Set 和 Map 不是为了替代数组和对象,而是为了在特定场景下提供更好的解决方案。
希望这篇文章能帮助你更好地理解和使用 JavaScript 中的 Set 和 Map。如果你有任何问题或想法,欢迎在评论区讨论!