大家好,我是今天的主讲人,很高兴能和大家聊聊 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. 判断元素存在性:Set
的 has
方法,快如闪电
判断元素是否存在,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
可以看到,在判断元素存在性方面,Set
的 has
方法比数组的 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 还引入了 WeakSet
。WeakSet
和 Set
的区别在于,WeakSet
只能存储对象,并且对对象的引用是弱引用。这意味着,如果一个对象只被 WeakSet
引用,那么垃圾回收器会自动回收这个对象,而不会造成内存泄漏。
WeakSet
的主要用途是跟踪对象的生命周期,比如在 DOM 元素被移除时,自动清理与之相关的资源。
总结:Set
,你的好帮手
总而言之,Set
是 JavaScript 中一个非常强大的数据结构,它在数组去重和判断元素存在性方面有着显著的性能优势。在合适的场景下,使用 Set
可以大大提高代码的效率和可读性。当然,Set
也有一些局限性,需要根据实际情况选择合适的数据结构。
下次当你需要对数组进行去重或者判断元素是否存在时,不妨考虑一下 Set
,它可能会给你带来意想不到的惊喜。记住,选择合适的工具,才能事半功倍。
互动环节:大家有什么问题吗?
(停顿,留出提问时间)
好的,看来大家对 Set
的理解都很到位。今天的分享就到这里,谢谢大家!