探讨 JavaScript 中的 new Set() 和 new Map() 在实现去重、查找、存储复杂数据类型时的性能考量和适用场景。

同学们,晚上好!我是今天的主讲人,很高兴能和大家一起聊聊 JavaScript 里两个非常实用的数据结构:SetMap。 它们就像工具箱里的瑞士军刀,看似简单,用起来却能解决各种复杂的问题。今天,我们就来深入了解一下它们在去重、查找、存储复杂数据类型等方面的性能考量和适用场景,力求让大家以后在项目中能灵活运用,事半功倍!

开场白:数据结构界的“老中医”和“新潮设计师”

大家平时写代码,肯定离不开数组、对象这些基础数据结构。但有时候,它们并不能很好地满足我们特定的需求。比如,数组去重效率不高,对象查找速度不够快,存储复杂数据类型又比较麻烦。这时候,SetMap 就派上用场了。

如果把数据结构比作医生,数组就像经验丰富的“老中医”,啥病都能开点药,但药效比较慢;对象就像“全科医生”,啥都懂一点,但不够专精。而 SetMap 就像“专科医生”,在特定领域有独到之处,能更快更精准地解决问题。

Set 就像一位“老中医”,专治各种“重复病”,擅长去重,保证数据的唯一性。Map 则像一位“新潮设计师”,擅长建立键值对的映射关系,能高效地查找和存储数据。

第一部分:Set——去重利器,快准狠!

Set 是一种集合数据结构,它的最大特点就是:不允许存储重复的值。 这使得它在去重方面具有天然的优势。

1.1 去重原理:基于哈希表的快速查找

Set 内部实现通常基于哈希表(Hash Table),这使得它在查找元素时具有非常高的效率,接近 O(1) 的时间复杂度。简单来说,哈希表就像一个巨大的索引表,每个元素都通过哈希函数映射到一个唯一的索引位置,查找时直接根据索引位置就能找到元素,大大提高了查找速度。

1.2 Set 去重 vs 传统数组去重:效率对比

传统数组去重的方法有很多,比如双重循环、indexOffilter 等,但这些方法的时间复杂度通常是 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 最常见的用途。
  • 判断元素是否存在: Sethas 方法可以快速判断元素是否存在。
  • 跟踪已处理过的元素: 例如,在爬虫程序中,可以使用 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:弱引用,避免内存泄漏

WeakMapMap 的一个变体,它的键必须是对象,并且是弱引用。这意味着,如果键对象没有被其他地方引用,垃圾回收器可以回收它,而 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
// 具体时间取决于垃圾回收器的运行机制

第三部分:SetMap 的进阶用法:集合运算、数据转换

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 SetMap 的数据转换:

  • 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);

第四部分:总结与最佳实践

今天,我们深入学习了 SetMap 这两个强大的数据结构,了解了它们在去重、查找、存储复杂数据类型等方面的性能考量和适用场景。

最佳实践:

  • 根据实际需求选择合适的数据结构: 不要盲目追求新技术,要根据实际需求选择最合适的数据结构。
  • 注意性能优化: 在处理大量数据时,要注意性能优化,尽量避免不必要的循环和重复计算。
  • 合理使用 WeakMap 在需要存储对象元数据,又不希望干扰垃圾回收时,可以使用 WeakMap
  • 了解浏览器的兼容性: SetMap 是 ES6 的新特性,要注意浏览器的兼容性,可以使用 Babel 等工具进行转换。

希望今天的分享能帮助大家更好地理解和运用 SetMap,在实际项目中写出更高效、更优雅的代码!

结束语:

SetMap 就像两把锋利的宝剑,掌握它们,你就能在数据结构的战场上披荆斩棘,无往不胜! 希望大家多多练习,熟练运用,下次再遇到类似问题时,就能自信地说:“这题我会!”

谢谢大家!

发表回复

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