同学们,晚上好!我是今天的主讲人,很高兴能和大家一起聊聊 JavaScript 里两个非常实用的数据结构:Set
和 Map
。 它们就像工具箱里的瑞士军刀,看似简单,用起来却能解决各种复杂的问题。今天,我们就来深入了解一下它们在去重、查找、存储复杂数据类型等方面的性能考量和适用场景,力求让大家以后在项目中能灵活运用,事半功倍!
开场白:数据结构界的“老中医”和“新潮设计师”
大家平时写代码,肯定离不开数组、对象这些基础数据结构。但有时候,它们并不能很好地满足我们特定的需求。比如,数组去重效率不高,对象查找速度不够快,存储复杂数据类型又比较麻烦。这时候,Set
和 Map
就派上用场了。
如果把数据结构比作医生,数组就像经验丰富的“老中医”,啥病都能开点药,但药效比较慢;对象就像“全科医生”,啥都懂一点,但不够专精。而 Set
和 Map
就像“专科医生”,在特定领域有独到之处,能更快更精准地解决问题。
Set
就像一位“老中医”,专治各种“重复病”,擅长去重,保证数据的唯一性。Map
则像一位“新潮设计师”,擅长建立键值对的映射关系,能高效地查找和存储数据。
第一部分:Set
——去重利器,快准狠!
Set
是一种集合数据结构,它的最大特点就是:不允许存储重复的值。 这使得它在去重方面具有天然的优势。
1.1 去重原理:基于哈希表的快速查找
Set
内部实现通常基于哈希表(Hash Table),这使得它在查找元素时具有非常高的效率,接近 O(1) 的时间复杂度。简单来说,哈希表就像一个巨大的索引表,每个元素都通过哈希函数映射到一个唯一的索引位置,查找时直接根据索引位置就能找到元素,大大提高了查找速度。
1.2 Set
去重 vs 传统数组去重:效率对比
传统数组去重的方法有很多,比如双重循环、indexOf
、filter
等,但这些方法的时间复杂度通常是 O(n^2) 或 O(n),当数据量很大时,效率会非常低下。
而 Set
去重的时间复杂度接近 O(n),效率更高。下面我们用代码来对比一下:
// 数组去重(传统方法)
function uniqueArray(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (result.indexOf(arr[i]) === -1) {
result.push(arr[i]);
}
}
return result;
}
// 数组去重(使用 Set)
function uniqueSet(arr) {
return [...new Set(arr)]; // 或者 Array.from(new Set(arr))
}
// 测试数据
const arr = Array.from({ length: 100000 }, () => Math.floor(Math.random() * 1000));
// 性能测试
console.time('Array unique');
uniqueArray(arr);
console.timeEnd('Array unique');
console.time('Set unique');
uniqueSet(arr);
console.timeEnd('Set unique');
运行结果可以明显看到,Set
去重的速度比传统数组去重快得多。
1.3 Set
的基本用法:增删查改
add(value)
: 添加一个值到Set
中。delete(value)
: 从Set
中删除一个值。has(value)
: 判断Set
中是否存在某个值。clear()
: 清空Set
中的所有值。size
: 返回Set
中值的数量。forEach(callbackfn, thisArg)
: 遍历Set
中的每个值。
示例代码:
const mySet = new Set();
mySet.add(1);
mySet.add(2);
mySet.add(3);
mySet.add(2); // 重复添加,Set 会自动去重
console.log(mySet.size); // 输出 3
console.log(mySet.has(2)); // 输出 true
mySet.delete(2);
console.log(mySet.has(2)); // 输出 false
mySet.forEach(value => {
console.log(value); // 输出 1, 3
});
mySet.clear();
console.log(mySet.size); // 输出 0
1.4 Set
的适用场景:
- 数组去重: 这是
Set
最常见的用途。 - 判断元素是否存在:
Set
的has
方法可以快速判断元素是否存在。 - 跟踪已处理过的元素: 例如,在爬虫程序中,可以使用
Set
记录已经爬取过的 URL,避免重复爬取。 - 集合运算: 虽然
Set
本身没有提供集合运算的方法,但我们可以利用Set
的特性实现交集、并集、差集等运算。
1.5 Set
的局限性:
- 无法直接访问特定位置的元素:
Set
没有索引,不能像数组那样通过下标访问元素。 - 存储顺序不确定:
Set
的存储顺序取决于哈希函数的实现,不保证与添加顺序一致。
第二部分:Map
——键值对的完美搭档,灵活高效!
Map
是一种键值对的数据结构,它允许使用任何类型的值作为键(包括对象、函数等)。 这使得它比传统的对象更加灵活。
2.1 Map
的优势:键的类型不局限于字符串
传统的 JavaScript 对象只能使用字符串或 Symbol 作为键,这在某些场景下会受到限制。比如,我们想用 DOM 元素作为键,或者用对象作为键,对象就无能为力了。
Map
则没有这个限制,它可以接受任何类型的值作为键,这使得它在存储复杂数据类型时更加方便。
2.2 Map
的基本用法:增删查改
set(key, value)
: 设置Map
中键对应的值。get(key)
: 获取Map
中键对应的值。delete(key)
: 从Map
中删除键值对。has(key)
: 判断Map
中是否存在某个键。clear()
: 清空Map
中的所有键值对。size
: 返回Map
中键值对的数量。forEach(callbackfn, thisArg)
: 遍历Map
中的每个键值对。
示例代码:
const myMap = new Map();
const objKey = { id: 1 };
const funcKey = () => {};
myMap.set(objKey, 'Object Value');
myMap.set(funcKey, 'Function Value');
myMap.set('stringKey', 'String Value');
console.log(myMap.get(objKey)); // 输出 'Object Value'
console.log(myMap.get(funcKey)); // 输出 'Function Value'
console.log(myMap.get('stringKey')); // 输出 'String Value'
console.log(myMap.has(objKey)); // 输出 true
myMap.delete(objKey);
console.log(myMap.has(objKey)); // 输出 false
myMap.forEach((value, key) => {
console.log(`Key: ${key}, Value: ${value}`);
});
myMap.clear();
console.log(myMap.size); // 输出 0
2.3 Map
的性能考量:哈希表的查找效率
与 Set
类似,Map
内部也通常基于哈希表实现,因此在查找、插入和删除键值对时具有很高的效率,接近 O(1) 的时间复杂度。
2.4 Map
的适用场景:
- 存储和查找复杂数据类型:
Map
可以使用任何类型的值作为键,非常适合存储和查找复杂数据类型。 - 缓存数据: 可以使用
Map
缓存计算结果或 API 响应,提高性能。 - 跟踪对象的属性: 可以使用
Map
跟踪对象的属性,例如,记录对象的访问次数或修改时间。 - 存储元数据: 可以将元数据与对象关联起来,例如,存储 DOM 元素的额外信息。
2.5 Map
vs 对象:选择哪一个?
在很多情况下,Map
和对象都可以用来存储键值对,那么我们应该选择哪一个呢?
特性 | 对象 (Object) | Map |
---|---|---|
键的类型 | 字符串或 Symbol | 任何类型 |
键的顺序 | 不保证 (ES2015 之后基本保持插入顺序,但仍不保证) | 保持插入顺序 |
原型属性 | 继承原型链上的属性 | 不继承原型链上的属性 |
键值对数量 | 手动计算 | 可以直接获取 size 属性 |
性能 | 在小型数据集上可能更快,但在大型数据集上 Map 更快 |
在大型数据集上性能更好,尤其是在频繁增删键值对的情况下 |
JSON 支持 | 可以直接使用 JSON.stringify 序列化 |
需要手动转换才能序列化 |
总结:
- 如果键的类型是字符串或 Symbol,且数据量不大,对象可能更方便。
- 如果键的类型是复杂数据类型,或者需要保持键的顺序,或者数据量很大,
Map
是更好的选择。
2.6 WeakMap:弱引用,避免内存泄漏
WeakMap
是 Map
的一个变体,它的键必须是对象,并且是弱引用。这意味着,如果键对象没有被其他地方引用,垃圾回收器可以回收它,而 WeakMap
中的对应键值对也会被自动删除。
WeakMap
的主要用途是:在不干扰垃圾回收的情况下,存储对象的元数据。
示例代码:
let obj = { id: 1 };
const weakMap = new WeakMap();
weakMap.set(obj, 'Some data');
console.log(weakMap.get(obj)); // 输出 'Some data'
obj = null; // 解除 obj 的引用
// 此时,如果 obj 没有被其他地方引用,垃圾回收器可以回收它
// 并且 weakMap 中的对应键值对也会被自动删除
// 稍后(在垃圾回收之后),weakMap.get(obj) 将返回 undefined
// 具体时间取决于垃圾回收器的运行机制
第三部分:Set
和 Map
的进阶用法:集合运算、数据转换
3.1 使用 Set
实现集合运算:
虽然 Set
本身没有提供集合运算的方法,但我们可以利用 Set
的特性来实现交集、并集、差集等运算。
- 交集:
function intersection(setA, setB) {
const intersectionSet = new Set();
for (const elem of setB) {
if (setA.has(elem)) {
intersectionSet.add(elem);
}
}
return intersectionSet;
}
- 并集:
function union(setA, setB) {
const unionSet = new Set(setA);
for (const elem of setB) {
unionSet.add(elem);
}
return unionSet;
}
- 差集:
function difference(setA, setB) {
const differenceSet = new Set(setA);
for (const elem of setB) {
differenceSet.delete(elem);
}
return differenceSet;
}
3.2 Set
和 Map
的数据转换:
Set
转数组:
const mySet = new Set([1, 2, 3]);
const myArray = [...mySet]; // 或者 Array.from(mySet)
- 数组转
Set
:
const myArray = [1, 2, 3];
const mySet = new Set(myArray);
Map
转对象:
function mapToObject(map) {
const obj = {};
for (const [key, value] of map) {
obj[key] = value; // 注意:这里只能将键转换为字符串或 Symbol
}
return obj;
}
- 对象转
Map
:
function objectToMap(obj) {
const map = new Map();
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
map.set(key, obj[key]); //对象本身的属性才添加到map里
}
}
return map;
}
3.3 使用 Map
实现更复杂的缓存:
除了简单的缓存计算结果,我们还可以使用 Map
实现更复杂的缓存策略,例如:
- 带有过期时间的缓存:
class ExpiringCache {
constructor(ttl = 60000) { // 默认过期时间 60 秒
this.cache = new Map();
this.ttl = ttl;
}
get(key) {
const item = this.cache.get(key);
if (!item) {
return undefined;
}
if (Date.now() > item.expiry) {
this.cache.delete(key);
return undefined;
}
return item.value;
}
set(key, value) {
const expiry = Date.now() + this.ttl;
this.cache.set(key, { value, expiry });
}
delete(key) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
}
const myCache = new ExpiringCache();
myCache.set('data', 'Some data');
setTimeout(() => {
console.log(myCache.get('data')); // 60 秒后输出 undefined
}, 70000);
第四部分:总结与最佳实践
今天,我们深入学习了 Set
和 Map
这两个强大的数据结构,了解了它们在去重、查找、存储复杂数据类型等方面的性能考量和适用场景。
最佳实践:
- 根据实际需求选择合适的数据结构: 不要盲目追求新技术,要根据实际需求选择最合适的数据结构。
- 注意性能优化: 在处理大量数据时,要注意性能优化,尽量避免不必要的循环和重复计算。
- 合理使用
WeakMap
: 在需要存储对象元数据,又不希望干扰垃圾回收时,可以使用WeakMap
。 - 了解浏览器的兼容性:
Set
和Map
是 ES6 的新特性,要注意浏览器的兼容性,可以使用 Babel 等工具进行转换。
希望今天的分享能帮助大家更好地理解和运用 Set
和 Map
,在实际项目中写出更高效、更优雅的代码!
结束语:
Set
和 Map
就像两把锋利的宝剑,掌握它们,你就能在数据结构的战场上披荆斩棘,无往不胜! 希望大家多多练习,熟练运用,下次再遇到类似问题时,就能自信地说:“这题我会!”
谢谢大家!