各位观众老爷,大家好!我是你们的老朋友,码农张三。今天咱们不聊风花雪月,就来唠唠嗑,聊聊 JavaScript 里两个实用的小伙伴:Set
和 Map
。 别看它们名字挺简单,用好了,能让你的代码效率嗖嗖地往上涨,还能让你的面试官眼前一亮,觉得你这小子/丫头有点东西!
开场白:Set
和 Map
,你们是来搞笑的吗?
很多人第一次接触 Set
和 Map
,可能觉得它们和数组、对象差不多,没什么特别的。甚至会觉得,"这玩意儿是来搞笑的吗?我已经有数组和对象了,还要你们干啥?"
别急,且听我慢慢道来。 Set
和 Map
就像是武器库里的两把瑞士军刀,看似不起眼,但在特定场景下,能发挥出意想不到的作用。 咱们先从 Set
开始说起。
第一部分:Set
的奇妙之旅:去重神器与集合运算
Set
,顾名思义,集合。它最大的特点就是:不允许重复元素。 这简直就是去重界的扛把子!
- 去重,so easy!
传统的数组去重,可能需要你写一堆循环判断,各种 indexOf
、includes
满天飞,代码又臭又长。 但有了 Set
,一切都变得简单粗暴:
const arr = [1, 2, 2, 3, 4, 4, 5];
const set = new Set(arr);
const uniqueArr = [...set]; // 或者 Array.from(set)
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
一行代码,搞定去重! 是不是感觉神清气爽? Set
的构造函数接受一个可迭代对象(比如数组),它会自动去除重复的元素。 然后,我们可以用展开运算符 (...
) 或者 Array.from()
方法,将 Set
转换回数组。
Set
的常用方法
Set
对象提供了一些常用的方法,方便我们操作集合:
* `add(value)`: 向集合中添加一个值。
* `delete(value)`: 从集合中删除一个值。返回 `true` 如果删除成功,否则返回 `false`。
* `has(value)`: 判断集合中是否存在某个值。返回 `true` 或 `false`。
* `clear()`: 清空集合中的所有值。
* `size`: 返回集合中值的个数。
const mySet = new Set();
mySet.add(1);
mySet.add(2);
mySet.add(2); // 添加重复的值,会被忽略
console.log(mySet.size); // 输出: 2
console.log(mySet.has(1)); // 输出: true
mySet.delete(1);
console.log(mySet.has(1)); // 输出: false
mySet.clear();
console.log(mySet.size); // 输出: 0
- 集合运算:并集、交集、差集
Set
不仅可以去重,还可以进行一些基本的集合运算。 虽然 JavaScript 没有直接提供并集、交集、差集的方法,但我们可以利用 Set
的特性,自己实现这些运算。
* **并集 (Union)**:包含所有集合中的元素。
function union(setA, setB) {
const _union = new Set(setA); // Copy setA
for (const elem of setB) {
_union.add(elem);
}
return _union;
}
const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);
const unionSet = union(setA, setB);
console.log([...unionSet]); // 输出: [1, 2, 3, 4, 5]
* **交集 (Intersection)**:包含所有集合中共同的元素。
function intersection(setA, setB) {
const _intersection = new Set();
for (const elem of setB) {
if (setA.has(elem)) {
_intersection.add(elem);
}
}
return _intersection;
}
const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);
const intersectionSet = intersection(setA, setB);
console.log([...intersectionSet]); // 输出: [3]
* **差集 (Difference)**:包含只存在于第一个集合,而不存在于第二个集合的元素。
function difference(setA, setB) {
const _difference = new Set(setA);
for (const elem of setB) {
_difference.delete(elem);
}
return _difference;
}
const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);
const differenceSet = difference(setA, setB);
console.log([...differenceSet]); // 输出: [1, 2]
Set
的性能优势
Set
的 has()
方法的平均时间复杂度是 O(1),这意味着无论 Set
中有多少元素,判断一个元素是否存在的时间几乎是恒定的。 这比数组的 includes()
方法(时间复杂度为 O(n))要快得多。
第二部分:Map
的妙用:键值对的进阶玩法
Map
对象保存键值对,任何值(对象或者原始值)都可以作为一个键或一个值。 简单来说,Map
就是一个升级版的对象。
Map
和对象的区别
你可能会问,既然有了对象,为什么还需要 Map
呢? Map
相比于对象,有以下几个优势:
* **键的类型**:对象的键只能是字符串或 Symbol,而 `Map` 的键可以是任何类型,包括对象、函数、甚至 NaN。
* **键的顺序**:`Map` 会保留键的插入顺序,而对象的键的顺序是不确定的(尤其是在 ES6 之前)。
* **键的数量**:可以通过 `Map` 的 `size` 属性直接获取键值对的数量,而对象的键值对数量需要手动计算。
* **性能**:在频繁添加和删除键值对的场景下,`Map` 的性能通常比对象更好。
Map
的常用方法
Map
对象提供了一些常用的方法,方便我们操作键值对:
* `set(key, value)`: 向 `Map` 中添加或更新一个键值对。
* `get(key)`: 返回与键关联的值,如果键不存在,则返回 `undefined`。
* `delete(key)`: 从 `Map` 中删除一个键值对。返回 `true` 如果删除成功,否则返回 `false`。
* `has(key)`: 判断 `Map` 中是否存在某个键。返回 `true` 或 `false`。
* `clear()`: 清空 `Map` 中的所有键值对。
* `size`: 返回 `Map` 中键值对的个数。
const myMap = new Map();
myMap.set('name', '张三');
myMap.set(1, '数字1');
myMap.set({}, '一个空对象');
console.log(myMap.get('name')); // 输出: 张三
console.log(myMap.get(1)); // 输出: 数字1
console.log(myMap.get({})); // 输出: undefined (因为键是不同的对象)
console.log(myMap.has('name')); // 输出: true
myMap.delete('name');
console.log(myMap.has('name')); // 输出: false
console.log(myMap.size); // 输出: 2
myMap.clear();
console.log(myMap.size); // 输出: 0
-
Map
的应用场景- 缓存:
Map
非常适合用于实现缓存,可以将计算结果缓存起来,下次直接从Map
中获取,避免重复计算。
- 缓存:
const cache = new Map();
function expensiveCalculation(input) {
if (cache.has(input)) {
console.log('从缓存中获取');
return cache.get(input);
}
console.log('进行计算');
// 模拟耗时计算
let result = 0;
for (let i = 0; i < 100000000; i++) {
result += input;
}
cache.set(input, result);
return result;
}
console.log(expensiveCalculation(5)); // 进行计算
console.log(expensiveCalculation(5)); // 从缓存中获取
console.log(expensiveCalculation(10)); // 进行计算
* **存储元数据**:可以将 DOM 元素或者其他对象与一些元数据关联起来,比如事件监听器、状态等等。
const element = document.getElementById('myElement');
const metadata = new Map();
metadata.set(element, {
listeners: [],
state: 'idle'
});
// ...
* **统计词频**:可以利用 `Map` 统计一段文本中每个词出现的次数。
function wordFrequency(text) {
const words = text.toLowerCase().split(/s+/); // 转换为小写,并按空格分割
const frequencyMap = new Map();
for (const word of words) {
if (frequencyMap.has(word)) {
frequencyMap.set(word, frequencyMap.get(word) + 1);
} else {
frequencyMap.set(word, 1);
}
}
return frequencyMap;
}
const text = 'This is a test. This is only a test.';
const frequency = wordFrequency(text);
console.log(frequency);
// 输出:
// Map(6) {
// 'this' => 2,
// 'is' => 2,
// 'a' => 2,
// 'test.' => 2,
// 'only' => 1,
// 'test' => 1
// }
Map
的性能优势
Map
的 get()
、set()
、delete()
和 has()
方法的平均时间复杂度都是 O(1),这使得 Map
在处理大量数据时,性能优于对象。
第三部分:WeakSet
和 WeakMap
:弱引用与内存管理
WeakSet
和 WeakMap
是 Set
和 Map
的变体,它们最大的特点是:弱引用。
- 什么是弱引用?
弱引用是指,如果一个对象只被 WeakSet
或 WeakMap
引用,那么垃圾回收机制会忽略这些引用,直接回收该对象。 也就是说,WeakSet
和 WeakMap
不会阻止垃圾回收。
-
WeakSet
的特点- 只能存储对象。
- 不能迭代(没有
forEach
方法,也没有size
属性)。 - 主要用于跟踪对象,当对象被回收后,
WeakSet
中会自动移除对该对象的引用。
-
WeakMap
的特点- 键必须是对象。
- 不能迭代(没有
forEach
方法,也没有size
属性)。 - 主要用于存储对象的元数据,当对象被回收后,
WeakMap
中会自动移除对该对象的引用。
-
WeakSet
和WeakMap
的应用场景- DOM 元素的元数据:可以使用
WeakMap
将 DOM 元素与一些元数据关联起来,当 DOM 元素被移除后,WeakMap
中会自动移除对该元素的引用,避免内存泄漏。
- DOM 元素的元数据:可以使用
const elementMetadata = new WeakMap();
const element = document.getElementById('myElement');
elementMetadata.set(element, {
state: 'active'
});
// 当 element 被移除后,elementMetadata 中对 element 的引用也会被自动移除
* **注册回调函数**:可以使用 `WeakSet` 跟踪注册的回调函数,当对象被回收后,自动移除相关的回调函数。
const registeredCallbacks = new WeakSet();
function registerCallback(obj, callback) {
registeredCallbacks.add(obj);
// ...
}
// 当 obj 被回收后,registeredCallbacks 中对 obj 的引用也会被自动移除
第四部分: 最佳实践与注意事项
- 选择合适的集合类型:根据实际需求选择合适的集合类型。 如果只需要去重,使用
Set
。 如果需要存储键值对,并且键的类型不确定,使用Map
。 如果需要存储对象的元数据,并且不希望阻止垃圾回收,使用WeakMap
。 - 避免内存泄漏:在使用
Map
存储 DOM 元素或者其他对象时,要注意手动移除不再需要的键值对,避免内存泄漏。 如果可以使用WeakMap
,则优先使用WeakMap
。 - 注意性能:在频繁添加和删除元素的场景下,
Set
和Map
的性能优于数组和对象。 - 理解键的相等性:
Set
和Map
使用的是严格相等 (===
) 来判断键的相等性。 这意味着,{}
和{}
是不同的键。 - 迭代顺序:
Set
和Map
会保留元素的插入顺序,可以使用for...of
循环或者forEach
方法来迭代元素。
总结:Set
和 Map
,你的代码小助手!
Set
和 Map
是 JavaScript 中两个非常实用的集合类型。 它们可以帮助我们更高效地处理数据,提高代码的可读性和可维护性。 掌握它们,你就能在编程的道路上更上一层楼!
好了,今天的讲座就到这里。 感谢大家的收看,希望大家能从中学到一些东西。 记住,码农的世界,没有最好,只有更好! 加油!