JS `Set` 与 `Map` 的实用技巧:去重、数据映射与性能优势

各位观众老爷,大家好!我是你们的老朋友,码农张三。今天咱们不聊风花雪月,就来唠唠嗑,聊聊 JavaScript 里两个实用的小伙伴:SetMap。 别看它们名字挺简单,用好了,能让你的代码效率嗖嗖地往上涨,还能让你的面试官眼前一亮,觉得你这小子/丫头有点东西!

开场白:SetMap,你们是来搞笑的吗?

很多人第一次接触 SetMap,可能觉得它们和数组、对象差不多,没什么特别的。甚至会觉得,"这玩意儿是来搞笑的吗?我已经有数组和对象了,还要你们干啥?"

别急,且听我慢慢道来。 SetMap 就像是武器库里的两把瑞士军刀,看似不起眼,但在特定场景下,能发挥出意想不到的作用。 咱们先从 Set 开始说起。

第一部分:Set 的奇妙之旅:去重神器与集合运算

Set,顾名思义,集合。它最大的特点就是:不允许重复元素。 这简直就是去重界的扛把子!

  • 去重,so easy!

传统的数组去重,可能需要你写一堆循环判断,各种 indexOfincludes 满天飞,代码又臭又长。 但有了 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 的性能优势

Sethas() 方法的平均时间复杂度是 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 的性能优势

Mapget()set()delete()has() 方法的平均时间复杂度都是 O(1),这使得 Map 在处理大量数据时,性能优于对象。

第三部分:WeakSetWeakMap:弱引用与内存管理

WeakSetWeakMapSetMap 的变体,它们最大的特点是:弱引用

  • 什么是弱引用?

弱引用是指,如果一个对象只被 WeakSetWeakMap 引用,那么垃圾回收机制会忽略这些引用,直接回收该对象。 也就是说,WeakSetWeakMap 不会阻止垃圾回收。

  • WeakSet 的特点

    • 只能存储对象。
    • 不能迭代(没有 forEach 方法,也没有 size 属性)。
    • 主要用于跟踪对象,当对象被回收后,WeakSet 中会自动移除对该对象的引用。
  • WeakMap 的特点

    • 键必须是对象。
    • 不能迭代(没有 forEach 方法,也没有 size 属性)。
    • 主要用于存储对象的元数据,当对象被回收后,WeakMap 中会自动移除对该对象的引用。
  • WeakSetWeakMap 的应用场景

    • DOM 元素的元数据:可以使用 WeakMap 将 DOM 元素与一些元数据关联起来,当 DOM 元素被移除后,WeakMap 中会自动移除对该元素的引用,避免内存泄漏。
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
  • 注意性能:在频繁添加和删除元素的场景下,SetMap 的性能优于数组和对象。
  • 理解键的相等性SetMap 使用的是严格相等 (===) 来判断键的相等性。 这意味着,{}{} 是不同的键。
  • 迭代顺序SetMap 会保留元素的插入顺序,可以使用 for...of 循环或者 forEach 方法来迭代元素。

总结:SetMap,你的代码小助手!

SetMap 是 JavaScript 中两个非常实用的集合类型。 它们可以帮助我们更高效地处理数据,提高代码的可读性和可维护性。 掌握它们,你就能在编程的道路上更上一层楼!

好了,今天的讲座就到这里。 感谢大家的收看,希望大家能从中学到一些东西。 记住,码农的世界,没有最好,只有更好! 加油!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注