数组是 JavaScript 中最常用的数据结构,但 ES6 引入的 Set 在许多场景下是更优的选择。两者看似都能存储一组值,底层设计和适用场景却有本质区别。选对数据结构,往往能让代码更清晰、性能更好。
核心差异
| 维度 | 数组 (Array) | Set |
|------|-------------|-----|
| 唯一性 | 允许重复值 | 自动去重,值唯一 |
| 顺序 | 严格有序,通过索引访问 | 有序(插入顺序),但无索引 |
| 查找 | indexOf / includes,O(n) | has,平均 O(1) |
| 插入 | push / unshift / splice | add |
| 删除 | splice / filter,需知道索引 | delete,直接按值删除 |
| 大小 | length 属性 | size 属性 |
| 遍历 | forEach、for...of、map 等 | forEach、for...of |
| 典型场景 | 有序集合、栈/队列、映射转换 | 去重、存在性检查、集合运算 |
性能对比(定性)
- 存在性检查:
Set.has()基于哈希实现,不受元素数量影响;Array.includes()需要遍历整个数组,元素越多越慢。当需要频繁判断“某个值是否存在”时,Set 的性能优势极其明显。 - 添加与删除:Set 的
add和delete都按值操作,而数组的splice需要先找到索引。在处理动态集合(频繁增删)时,Set 代码更简洁,性能也通常更好。 - 内存占用:Set 内部使用哈希表存储唯一值,相比数组会有一定的内存开销。在小数据量场景下差异可忽略;在极大量数据(百万级别)且不需要唯一性保证时,数组可能更省内存。
- 按索引访问:数组可以直接
arr[0],而 Set 没有索引概念,无法直接取“第 n 个”。如果需要按位置操作,数组是唯一选择。
实际代码对比
// 去重:Set 一行搞定
const arr = [1, 2, 2, 3];
const uniqueArr = [...new Set(arr)]; // [1, 2, 3]
// 存在性检查
// 数组每次都要遍历
if (arr.includes(someValue)) { ... }
// Set 直接查哈希
const set = new Set(arr);
if (set.has(someValue)) { ... }
// 删除指定值
// 数组需要先找索引再删除
const index = arr.indexOf(value);
if (index > -1) arr.splice(index, 1);
// Set 直接按值删除
set.delete(value);
选型建议
优先选 Set 的场景:
- 需要集合中的元素唯一(去重、标签集合、ID 集合)。
- 频繁执行存在性检查(
has),且数据量较大。 - 需要高效的按值增删,不关心位置。
- 想要进行集合运算(交集、并集、差集)——Set 可以配合简短的函数实现。
继续用数组的场景:
- 需要有序存储并依赖索引访问(如列表渲染、分页)。
- 需要重复元素(如统计频次、购物车数量)。
- 需要大量数组特有的方法链(
map→filter→reduce)。 - 需要对数据进行排序(数组有
sort,Set 需先转为数组)。 - 与现有代码兼容——很多库和旧接口返回的是数组。
真实项目中的权衡:
- 集合运算示例:可以用
new Set([...setA].filter(x => setB.has(x)))实现交集,但数据量极大时需注意转换开销。对于百万级数据,原生 Set 操作仍然很快,转为数组再转回 Set 会带来额外内存和遍历成本,应尽量直接迭代 Set。 - 内存敏感场景:如果仅需要临时去重后立即转为数组使用,
[...new Set(array)]是简洁且性能可接受的写法,Set 中间态会被垃圾回收。
一个常见误区的澄清
“Set 比数组更快”这个说法并不全面。Set 在按值存取上确实快,但若你的需求是遍历所有元素进行转换,数组的 map 原生优化同样出色,且无需额外哈希计算。数据结构没有绝对好坏,只有是否匹配当前操作模式。根据实际的读写比例和访问模式选择,才是最务实的做法。