JS `Set` 在数组去重与判断元素存在性上的性能优势

大家好,我是今天的主讲人,很高兴能和大家聊聊 JavaScript 中 Set 对象在数组去重和判断元素存在性方面的性能优势。咱们今天就来一起扒一扒 Set 的底裤,看看它到底有什么魔力,能在这两个方面甩数组几条街。

开场白:数组,你还好吗?

在 JavaScript 的世界里,数组算得上是老前辈了,陪伴我们走过了无数个日夜,承载了各种各样的数据。但老话说得好,人老了就得服老,数组在某些方面,确实有点力不从心了。尤其是在面对海量数据时,它的性能瓶颈就暴露无遗了。

想象一下,你要在一个包含成千上万个元素的数组里查找某个元素是否存在,或者要对这个数组进行去重,你该怎么办?传统的做法,要么用 indexOf 或者 includes 循环遍历,要么用 filter + indexOf 或者嵌套循环来实现去重。这些方法,效率可想而知,简直惨不忍睹。

Set:闪亮登场,自带主角光环

这个时候,Set 对象就像一位救世主一样,带着主角光环闪亮登场了。Set 是 ES6 引入的一种新的数据结构,它类似于数组,但成员的值都是唯一的,没有重复的值。这就意味着,Set 天然就具备去重的能力。更重要的是,Set 在判断元素存在性方面的性能,比数组要高出几个数量级。

Set 的底层实现:哈希表,速度的保证

Set 之所以能这么快,关键在于它的底层实现。大多数 JavaScript 引擎(比如 V8,也就是 Chrome 和 Node.js 用的引擎)都使用哈希表(Hash Table)来实现 Set。哈希表是一种非常高效的数据结构,它可以在接近 O(1) 的时间复杂度内完成插入、删除和查找操作。

简单来说,哈希表就像一个装东西的柜子,每个格子都有一个唯一的编号(哈希值)。当你要把一个东西放进柜子里时,哈希函数会根据这个东西的特征计算出一个哈希值,然后把这个东西放到对应的格子里。当你需要查找这个东西时,只要再次计算出它的哈希值,就能直接找到它所在的格子,而不需要遍历整个柜子。

数组的劣势:线性查找,时间复杂度 O(n)

相比之下,数组的查找操作通常需要遍历整个数组,也就是所谓的线性查找。这意味着,如果数组有 n 个元素,那么最坏情况下,你需要比较 n 次才能找到目标元素。时间复杂度是 O(n),随着 n 的增大,查找时间会线性增长。

代码说话:Set vs 数组,性能大比拼

光说不练假把式,咱们用代码来实际测试一下 Set 和数组在去重和判断元素存在性方面的性能差异。

1. 数组去重:Set 一行代码搞定

数组去重,用 Set 简直不要太方便。

const array = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5];

// 使用 Set 去重
const uniqueArray = [...new Set(array)];

console.log(uniqueArray); // [1, 2, 3, 4, 5]

一行代码,搞定!简洁明了,优雅至极。

如果用传统的数组方法,可能需要这样:

const array = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5];

// 使用 filter 和 indexOf 去重
const uniqueArray = array.filter((item, index) => array.indexOf(item) === index);

console.log(uniqueArray); // [1, 2, 3, 4, 5]

或者:

const array = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5];
const uniqueArray = [];
for (let i = 0; i < array.length; i++) {
  if (uniqueArray.indexOf(array[i]) === -1) {
    uniqueArray.push(array[i]);
  }
}
console.log(uniqueArray);

代码量明显多了不少,而且效率也更低。

2. 判断元素存在性:Sethas 方法,快如闪电

判断元素是否存在,Set 提供了 has 方法,时间复杂度接近 O(1)。

const set = new Set([1, 2, 3, 4, 5]);

// 使用 Set 的 has 方法判断元素是否存在
console.log(set.has(3)); // true
console.log(set.has(6)); // false

数组呢?只能用 indexOf 或者 includes,时间复杂度 O(n)。

const array = [1, 2, 3, 4, 5];

// 使用数组的 indexOf 方法判断元素是否存在
console.log(array.indexOf(3) !== -1); // true
console.log(array.indexOf(6) !== -1); // false

// 使用数组的 includes 方法判断元素是否存在
console.log(array.includes(3)); // true
console.log(array.includes(6)); // false

性能测试:真金不怕火炼

为了更直观地展示 Set 和数组的性能差异,咱们来做一个简单的性能测试。

const arraySize = 100000;
const array = Array.from({ length: arraySize }, () => Math.floor(Math.random() * arraySize));
const set = new Set(array);
const targetValue = Math.floor(Math.random() * arraySize);

console.time('Array includes');
for (let i = 0; i < 1000; i++) {
  array.includes(targetValue);
}
console.timeEnd('Array includes');

console.time('Set has');
for (let i = 0; i < 1000; i++) {
  set.has(targetValue);
}
console.timeEnd('Set has');

运行结果(仅供参考,不同环境结果可能不同):

Array includes: 10ms - 20ms
Set has: 0.1ms - 0.5ms

可以看到,在判断元素存在性方面,Sethas 方法比数组的 includes 方法快了 几十倍甚至上百倍

表格总结:Set vs 数组,优劣一览

特性 Set 数组
元素唯一性 保证元素唯一,自动去重 允许重复元素
查找效率 接近 O(1) O(n)
插入/删除效率 接近 O(1) 插入/删除元素可能需要移动其他元素,效率较低
适用场景 需要快速查找、去重的场景 各种场景,但大数据量时性能较差
语法 add, delete, has, size 等方法 push, pop, shift, unshift 等方法

Set 的局限性:并非万能

虽然 Set 在去重和判断元素存在性方面有着显著的优势,但它也并非万能。

  • 无法通过索引访问元素: Set 没有索引的概念,无法像数组一样通过 set[0] 的方式访问元素。
  • 元素顺序: Set 元素的顺序是按照插入顺序排列的,但并非所有的 JavaScript 引擎都保证这一点。
  • 不支持复杂对象: 如果 Set 中存储的是对象,那么比较的是对象的引用,而不是对象的内容。这意味着,即使两个对象的内容相同,但只要它们的引用不同,Set 仍然会认为它们是不同的元素。要解决这个问题,需要使用一些技巧,比如将对象序列化成字符串再存储到 Set 中。

高级用法:Set 和其他数据结构的结合

Set 可以与其他数据结构结合使用,发挥更大的威力。

  • Set + Map Set 用于存储键,Map 用于存储键值对,可以实现更复杂的数据结构。
  • Set + 数组: Set 用于去重,数组用于存储有序数据,可以兼顾去重和顺序。

WeakSet:弱引用,避免内存泄漏

除了 Set 之外,ES6 还引入了 WeakSetWeakSetSet 的区别在于,WeakSet 只能存储对象,并且对对象的引用是弱引用。这意味着,如果一个对象只被 WeakSet 引用,那么垃圾回收器会自动回收这个对象,而不会造成内存泄漏。

WeakSet 的主要用途是跟踪对象的生命周期,比如在 DOM 元素被移除时,自动清理与之相关的资源。

总结:Set,你的好帮手

总而言之,Set 是 JavaScript 中一个非常强大的数据结构,它在数组去重和判断元素存在性方面有着显著的性能优势。在合适的场景下,使用 Set 可以大大提高代码的效率和可读性。当然,Set 也有一些局限性,需要根据实际情况选择合适的数据结构。

下次当你需要对数组进行去重或者判断元素是否存在时,不妨考虑一下 Set,它可能会给你带来意想不到的惊喜。记住,选择合适的工具,才能事半功倍。

互动环节:大家有什么问题吗?

(停顿,留出提问时间)

好的,看来大家对 Set 的理解都很到位。今天的分享就到这里,谢谢大家!

发表回复

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