JS 中 Set 和 Array 有什么区别?何时该用 Set

作者:拾光

时间:2026-10-30

浏览:517

深入解析 JavaScript 中 Set 与 Array 的核心区别,从数据模型、唯一性、检索复杂度到实际业务场景,给出明确的选型与使用指南。

文章目录
核心结论速览: Array 是有序的线性索引列表,允许元素重复,擅长通过下标精准存取、执行链式数据变换(如 map/filter)以及维持严格的业务排序;而 Set 是基于值的唯一集合,天然强制去重,底层依赖类哈希表的散列机制,在元素存在性检查(has)和增删操作上拥有常数级(接近 O(1))的高效表现。当业务侧重于“数据唯一性”或“高频查找判定”时应果断使用 Set;若依赖“数字索引”、“前后顺序控制”或“JSON 直接序列化”,则必须选择 Array。
JS 中 Set 和 Array 有什么区别?何时该用 Set
图 0:Set 与 Array 核心特征概览

一、核心模型:底层存储与索引机制差异

要透彻理解两者的区别,首先需要厘清它们在 JavaScript 引擎中所代表的完全不同的数据模型。

Array(数组)是 ECMAScript 中最经典、使用最广泛的线性数据结构。数组中的每个元素都有一个从 0 开始递增的整数下标(Index)。在 V8 等现代引擎中,如果数组元素类型密集,通常会被分配为连续内存以获得极致的访问速度。数组允许存放任意数量的相同值,并完全以插入或指定的顺序保留每一个元素。

Set(集合)是 ES6(ECMAScript 2015)正式引入的内建对象。它是一组唯一值的集合。Set 内部并不维护“下标 0、下标 1”这样的整数索引,而是将存储的“值”本身作为散列标识。当你试图向 Set 中插入已经存在的值时,引擎会自动检测并丢弃该重复写入操作。

JS 中 Set 和 Array 有什么区别?何时该用 Set
图 1:Array 与 Set 的底层组织与查找机制对比

如上图所示,由于两者底层架构的差异,导致了最关键的操作特质分化:

  • 访问方式: Array 支持 arr[0] 这种根据偏移量直接寻址的快速随机访问;Set 无法通过数值下标读写,要获取内部元素必须通过迭代器(如 for...of、set.values())或将其展开。
  • 唯一性控制: Array 不对内容重复性做任何拦截,重复插入会直接拉长数组;Set 无论写入多少次相同值,其尺寸 size 始终严格等于唯一值的个数。
  • 迭代顺序: 两者在现代规范中都保证了插入顺序(Insertion Order)。使用 for...of 遍历 Set 时,元素会按照首次被 add() 进来的先后顺序依次输出。

二、性能量化:何时 Set 比 Array 快几个数量级?

在日常开发中,许多开发者习惯于使用 array.includes(target) 或 array.indexOf(target) !== -1 来判断一个元素是否存在。在数据量只有几十个时,这种写法的耗时微乎其微;但一旦数据规模增长到成千上万,两者的性能差距会变得非常悬殊。

Array 的查找本质是线性扫描(Linear Search)。引擎必须从索引 0 开始逐个比对,直到找到目标值或遍历至末尾,其时间复杂度为 O(N)。如果一个包含 100,000 个 ID 的数组,在每次用户交互时都要做多次 includes 查询,总比较次数会呈乘积级爆炸。

与之相反,Set 的底层是基于散列/哈希表构建的。通过 set.has(target) 进行匹配时,引擎能够通过散列函数直接计算存储桶位置,平均时间复杂度仅为 O(1)(常数时间)。无论 Set 中存放了 10 个还是 100,000 个元素,判断它是否存在的耗时几乎完全相同。

// 性能对比场景伪代码与原理演示
const count = 100000;
const rawList = Array.from({ length: count }, (_, i) => `item_${i}`);

const testArray = [...rawList];
const testSet = new Set(rawList);

// 场景:高频检索不存在或位于末尾的元素
console.time("Array includes");
testArray.includes("item_99999"); // 需要扫描接近 100,000 次
console.timeEnd("Array includes"); // 耗时通常在数毫秒至数十毫秒不等

console.time("Set has");
testSet.has("item_99999");        // 散列直接定位,耗时约为 0.01 毫秒
console.timeEnd("Set has");        // 差距可达数百至数千倍

同样地,对于元素的删除操作:Array 使用 splice 移除元素后,后续所有元素在内存中的下标都必须向前移动一位,开销同样为 O(N);而 Set 提供的 set.delete(val) 只需要定位并置空对应的散列槽位,开销稳定在 O(1)。

三、实战指南:Set 的核心用法与典型场景

1. 数组极速去重

将重复数据清洗为唯一列表是 Set 最广为人知的用途。得益于 ES6 展开运算符(Spread Operator)与 Array.from,开发者可以只用一行代码完成去重并回归数组形态。

// 基础数据类型去重
const duplicatedNumbers = [1, 3, 5, 3, 2, 1, 4, 5];
const uniqueNumbers = [...new Set(duplicatedNumbers)];
// 输出结果: [1, 3, 5, 2, 4](严格保留首次出现的顺序)
JS 中 Set 和 Array 有什么区别?何时该用 Set
图 2:数组去重与类型互转的标准操作流程

2. 高频交互状态判定(选中态、白名单、黑名单)

在前端交互界面中,我们经常需要维护“被用户选中的行/卡片 ID”或“只读字段白名单”。在列表项快速点击、全选、反选等频繁触发的场景下,将已选 ID 存储在 Set 中能带来明显的响应提升。

// 维护列表选中项的 ID 集合
const selectedIds = new Set();

// 切换选中状态
function toggleSelection(id) {
  if (selectedIds.has(id)) {
    selectedIds.delete(id);
  } else {
    selectedIds.add(id);
  }
}

// 渲染判断(在成千上万行渲染循环中极其轻快)
function isRowSelected(id) {
  return selectedIds.has(id); // O(1) 瞬时判断
}

3. 数学集合运算:并集、交集与差集

在权限比对、标签过滤和好友关系匹配中,经常需要计算两个集合的共有项或差异项。借助 Set 的 has 配合迭代,可以用非常干净的声明式语法完成运算。

JS 中 Set 和 Array 有什么区别?何时该用 Set
图 3:基于 Set 的常用数学集合运算模型
const setA = new Set(["React", "Vue", "Node"]);
const setB = new Set(["Vue", "Angular", "Node"]);

// 并集 (Union)
const union = new Set([...setA, ...setB]);
// Set(4) { "React", "Vue", "Node", "Angular" }

// 交集 (Intersection)
const intersection = new Set([...setA].filter(x => setB.has(x)));
// Set(2) { "Vue", "Node" }

// 差集 (Difference: 属于 A 但不属于 B)
const difference = new Set([...setA].filter(x => !setB.has(x)));
// Set(1) { "React" }
规范演进注记 (ECMAScript 2024 / 2025)
现代 JavaScript 已经正式为 Set 原型引入了官方集合运算方法,包括 setA.union(setB)、setA.intersection(setB)、setA.difference(setB)、setA.isSubsetOf(setB) 等。在现代浏览器(Chrome 122+、Safari 17+)或 Node.js 22+ 环境中,可直接调用原生方法而无需手动展开转数组。

四、避坑指南:使用 Set 必须警惕的边界陷阱

1. 引用类型的判等陷阱(SameValueZero 规则)

Set 内部判定两个值是否相同所采用的是 SameValueZero 算法。这种机制与严格相等运算符 === 极为接近,唯一的微小差别在于它认为 NaN 与 NaN 是相等的,且 +0 与 -0 是相等的。

极易踩坑的地方在于对象与数组等引用类型:

const objectSet = new Set();

objectSet.add({ id: 101 });
objectSet.add({ id: 101 });

console.log(objectSet.size); // 输出 2!未能去重!
console.log(objectSet.has({ id: 101 })); // 输出 false!

原因在于:两个独立字面量 { id: 101 } 在堆内存中开辟了不同的存储地址,两者的引用指针不同。如果你需要对包含复杂对象的数组按某字段去重,单纯用 new Set(arr) 是无效的,必须借助 Map 记录唯一 key,或手动维护唯一标识。

2. JSON 序列化缺失

JSON.stringify() 无法原生直接序列化 Set。如果直接将包含 Set 的对象传给序列化函数,Set 会被简单地转换为一个空对象 {}:

const payload = {
  username: "alice",
  roles: new Set(["admin", "editor"])
};

console.log(JSON.stringify(payload));
// 结果是: '{"username":"alice","roles":{}}' —— roles 中的数据全部丢失!

// 正确做法:在序列化前显式转回数组
payload.roles = Array.from(payload.roles);
console.log(JSON.stringify(payload));
// 结果: '{"username":"alice","roles":["admin","editor"]}'

3. 原型方法缺失与内存占用

Set 没有 map、filter、reduce、slice、sort 等数组高频操作方法。当需要对集合中的各项数据进行变换或重排序时,必须先将 Set 转为 Array。此外,Set 为了实现 O(1) 散列定位,其内部节点需要额外的哈希桶和链表指针空间,相同数据量下 Set 占用的内存通常比紧凑的原生数组要高出 30% 到 100%。

五、选型准则:何时应该坚守使用 Array?

尽管 Set 在特定场景下优势显著,但在日常大部分业务开发中,Array 依旧是绝对的主角。在以下场景下,你不应该盲目替换为 Set:

  1. 需要按索引精确定位: 如果业务逻辑包含“取第 1 个元素”、“取最后 1 个元素”、“通过滑块跳转到第 N 项”,或者需要使用二分查找法,必须使用具备下标机制的 Array。
  2. 需要排序与位置调整: 列表拖拽重排、按时间/金额升序降序排列(sort)、元素翻转(reverse)等操作,均基于连续索引展开。
  3. 本身包含合法重复数据: 例如用户操作日志(多次点击相同按钮)、财务流水、聊天消息流等。这类业务本身不允许去重,必须完整保留每一次重复发生的事实。
  4. 频繁的多步数据流处理: 需要大量链式调用 .map().filter().reduce().flatMap() 的数据清洗工作,Array 提供的原生方法生态最为完善直观。

六、全景选型决策树与对照总结

为了在实际工程开发中快速做出最清晰的抉择,可以通过下面的决策路径图与对照表进行综合研判:

JS 中 Set 和 Array 有什么区别?何时该用 Set
图 4:Array 与 Set 开发选型决策路径图
比对维度 Array (数组) Set (集合) 工程选型建议
数据唯一性 允许任意重复值 强制唯一,自动去重 需要无重复名单时用 Set,需保留重复记录用 Array
随机索引访问 支持 arr[index],O(1) 不支持下标访问 需要按位置/索引存取元素时必须使用 Array
存在性判断 (has) includes() 为 O(N) 线性扫描 has() 为 O(1) 散列定位 高频次在海量数据中检索某项是否存在时首选 Set
元素删除 (delete) splice() 需移动后续下标,O(N) delete() 散列标记,O(1) 频繁动态新增和移除大量 ID 状态时使用 Set
内置方法丰富度 拥有 map, filter, sort, slice 等几十个方法 仅基础增删查清 (add, delete, has, clear) 重度数据管道变换用 Array;简单集合操作用 Set
JSON 直接序列化 完全原生支持序列化为 [...] 原生 JSON.stringify 会输出为 {} 前后端网络接口传输、本地存储前务必使用 Array

总结: 在现代 JavaScript 架构中,Set 和 Array 绝非替代关系,而是互补的利器。最优秀的实践往往是“取长补短、协同使用”:在需要去重和高频判定的内部环节中把数据交给 Set 处理,在需要对外序列化传输、进行复杂排序或交给视图模板循环渲染时,再顺畅地还原为 Array。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章