JavaScript 提供了 Array 和 Set 来存储数据集合,虽然两者都能存储多个值,但在特定场景下,Set 具有显著的优势。让我们深入分析这两种数据结构的区别。
1. 数据唯一性:Set 的核心优势
1.1 重复值处理
Array(数组)
数组允许存储重复元素,这在某些场景下可能导致数据冗余:
javascript
const userIds = [1, 2, 2, 3, 3, 3, 4];
console.log(userIds); // [1, 2, 2, 3, 3, 3, 4]
console.log(userIds.length); // 7
// 需要手动去重
const uniqueIds = [...new Set(userIds)];
console.log(uniqueIds); // [1, 2, 3, 4]
Set(集合)
Set 自动确保数据唯一性,无需额外处理:
arduino
const userIds = new Set([1, 2, 2, 3, 3, 3, 4]);
console.log(userIds); // Set(4) {1, 2, 3, 4}
console.log(userIds.size); // 4
// 添加重复值会被忽略
userIds.add(2);
console.log(userIds.size); // 仍然是 4
1.2 特殊值处理
Set 能正确处理特殊值的唯一性:
javascript
const specialValues = new Set([NaN, NaN, 0, -0, undefined, null]);
console.log(specialValues); // Set(4) {NaN, 0, undefined, null}
// 注意:Set 认为 NaN === NaN,0 === -0
2. 性能对比:查找和删除操作
2.1 查找性能
Array 的查找
javascript
const largeArray = Array.from({length: 100000}, (_, i) => i);
// 时间复杂度 O(n)
console.time('Array includes');
const found = largeArray.includes(99999);
console.timeEnd('Array includes'); // 较慢
// indexOf 也是 O(n)
const index = largeArray.indexOf(99999);
Set 的查找
javascript
const largeSet = new Set(Array.from({length: 100000}, (_, i) => i));
// 时间复杂度 O(1)
console.time('Set has');
const exists = largeSet.has(99999);
console.timeEnd('Set has'); // 更快
2.2 删除性能
Array 的删除
ini
const arr = [1, 2, 3, 4, 5];
// 删除特定值需要先找到索引,然后删除
const index = arr.indexOf(3);
if (index > -1) {
arr.splice(index, 1); // O(n) 时间复杂度
}
console.log(arr); // [1, 2, 4, 5]
Set 的删除
arduino
const set = new Set([1, 2, 3, 4, 5]);
// 直接删除,O(1) 时间复杂度
set.delete(3);
console.log(set); // Set(4) {1, 2, 4, 5}
3. API 设计:简洁性对比
3.1 基本操作
Array 的操作
ini
const arr = [];
// 添加元素
arr.push(1);
arr.push(2);
// 检查是否存在
const exists = arr.includes(1); // O(n)
// 删除元素
const index = arr.indexOf(1);
if (index > -1) arr.splice(index, 1);
// 获取长度
const length = arr.length;
// 清空
arr.length = 0; // 或 arr.splice(0)
Set 的操作
arduino
const set = new Set();
// 添加元素
set.add(1);
set.add(2);
// 检查是否存在
const exists = set.has(1); // O(1)
// 删除元素
set.delete(1); // O(1)
// 获取大小
const size = set.size;
// 清空
set.clear();
3.2 链式调用
Set 支持链式调用,代码更简洁:
sql
const result = new Set()
.add(1)
.add(2)
.add(3);
console.log(result); // Set(3) {1, 2, 3}
4. 迭代和遍历
4.1 迭代方式对比
Array 的迭代
ini
const arr = [1, 2, 3, 4, 5];
// 多种迭代方式
arr.forEach((item, index) => console.log(item, index));
for (let i = 0; i < arr.length; i++) {
console.log(arr[i], i);
}
for (const item of arr) {
console.log(item);
}
// 数组方法
const doubled = arr.map(x => x * 2);
const evens = arr.filter(x => x % 2 === 0);
const sum = arr.reduce((a, b) => a + b, 0);
Set 的迭代
dart
const set = new Set([1, 2, 3, 4, 5]);
// Set 的迭代方式
set.forEach(item => console.log(item));
for (const item of set) {
console.log(item);
}
// 转换为数组后使用数组方法
const doubled = [...set].map(x => x * 2);
const evens = [...set].filter(x => x % 2 === 0);
const sum = [...set].reduce((a, b) => a + b, 0);
4.2 保持插入顺序
两者都保持插入顺序,但 Set 的顺序保证更可靠:
csharp
const set = new Set();
set.add('first');
set.add('second');
set.add('third');
console.log([...set]); // ['first', 'second', 'third']
5. 实际应用场景
5.1 数据去重
ini
// 数组去重 - Set 方案
function removeDuplicates(arr) {
return [...new Set(arr)];
}
const duplicates = [1, 2, 2, 3, 3, 4, 4, 5];
const unique = removeDuplicates(duplicates);
console.log(unique); // [1, 2, 3, 4, 5]
// 对象数组去重
function removeDuplicateObjects(arr, key) {
const seen = new Set();
return arr.filter(item => {
const value = item[key];
if (seen.has(value)) {
return false;
}
seen.add(value);
return true;
});
}
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }, // 重复
{ id: 3, name: 'Charlie' }
];
const uniqueUsers = removeDuplicateObjects(users, 'id');
console.log(uniqueUsers);
5.2 集合运算
javascript
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// 并集
const union = new Set([...setA, ...setB]);
console.log(union); // Set(6) {1, 2, 3, 4, 5, 6}
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set(2) {3, 4}
// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set(2) {1, 2}
// 对称差集
const symmetricDiff = new Set([
...[...setA].filter(x => !setB.has(x)),
...[...setB].filter(x => !setA.has(x))
]);
console.log(symmetricDiff); // Set(4) {1, 2, 5, 6}
5.3 权限和标签管理
javascript
// 用户权限管理
class UserPermissions {
constructor() {
this.permissions = new Set();
}
addPermission(permission) {
this.permissions.add(permission);
return this; // 支持链式调用
}
removePermission(permission) {
this.permissions.delete(permission);
return this;
}
hasPermission(permission) {
return this.permissions.has(permission); // O(1) 查找
}
getAllPermissions() {
return [...this.permissions];
}
}
const user = new UserPermissions()
.addPermission('read')
.addPermission('write')
.addPermission('delete');
console.log(user.hasPermission('write')); // true
console.log(user.getAllPermissions()); // ['read', 'write', 'delete']
5.4 缓存和记录访问
javascript
// 访问记录管理
class VisitTracker {
constructor() {
this.visitedPages = new Set();
}
visit(page) {
this.visitedPages.add(page);
}
hasVisited(page) {
return this.visitedPages.has(page);
}
getVisitCount() {
return this.visitedPages.size;
}
getVisitedPages() {
return [...this.visitedPages];
}
}
const tracker = new VisitTracker();
tracker.visit('/home');
tracker.visit('/about');
tracker.visit('/home'); // 重复访问不会增加计数
console.log(tracker.getVisitCount()); // 2
console.log(tracker.hasVisited('/home')); // true
6. 性能基准测试
javascript
// 性能测试函数
function performanceTest() {
const size = 100000;
const testData = Array.from({length: size}, (_, i) => i);
// Array 性能测试
console.time('Array creation');
const arr = [...testData];
console.timeEnd('Array creation');
console.time('Array lookup');
for (let i = 0; i < 1000; i++) {
arr.includes(Math.floor(Math.random() * size));
}
console.timeEnd('Array lookup');
// Set 性能测试
console.time('Set creation');
const set = new Set(testData);
console.timeEnd('Set creation');
console.time('Set lookup');
for (let i = 0; i < 1000; i++) {
set.has(Math.floor(Math.random() * size));
}
console.timeEnd('Set lookup');
}
performanceTest();
7. 使用场景决策指南
场景 | 推荐数据结构 | 原因 |
---|---|---|
需要确保数据唯一性 | Set | 自动去重,避免重复数据 |
频繁的成员检测 | Set | O(1) 时间复杂度,性能优异 |
需要索引访问 | Array | 支持通过索引直接访问元素 |
需要数组方法(map、filter等) | Array | 丰富的内置方法 |
集合运算(交集、并集等) | Set | 天然支持集合操作 |
需要存储重复值 | Array | Set 会自动去重 |
权限管理 | Set | 快速检查权限存在性 |
标签系统 | Set | 避免重复标签,快速查找 |
访问记录 | Set | 自动去重,快速检查 |
数据处理和转换 | Array | 更多的处理方法 |
8. 最佳实践建议
8.1 选择 Set 的情况
javascript
// ✅ 好的使用场景
// 1. 去重操作
const uniqueValues = new Set(arrayWithDuplicates);
// 2. 快速成员检测
const allowedUsers = new Set(['admin', 'user', 'guest']);
if (allowedUsers.has(currentUser)) {
// 允许访问
}
// 3. 标签管理
const tags = new Set();
tags.add('javascript').add('frontend').add('tutorial');
// 4. 集合运算
const commonInterests = new Set(
[...userA.interests].filter(x => userB.interests.has(x))
);
8.2 选择 Array 的情况
ini
// ✅ 好的使用场景
// 1. 需要索引访问
const items = ['first', 'second', 'third'];
console.log(items[1]); // 'second'
// 2. 需要数组方法
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(x => x * 2);
const sum = numbers.reduce((a, b) => a + b, 0);
// 3. 需要保持重复值
const scores = [85, 90, 85, 92, 85]; // 允许重复分数
// 4. 复杂数据处理
const users = [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}];
const names = users.map(user => user.name);
8.3 混合使用
kotlin
// 结合两者优势
class DataManager {
constructor() {
this.items = []; // 保持顺序和重复值
this.uniqueIds = new Set(); // 快速检查唯一性
}
addItem(item) {
if (!this.uniqueIds.has(item.id)) {
this.items.push(item);
this.uniqueIds.add(item.id);
return true;
}
return false; // 已存在
}
hasItem(id) {
return this.uniqueIds.has(id); // O(1) 查找
}
getItemByIndex(index) {
return this.items[index]; // O(1) 索引访问
}
getAllItems() {
return [...this.items]; // 返回副本
}
}
9. 总结
Set 和 Array 各有优势,选择哪个取决于具体需求:
选择 Set 当你需要:
- • 确保数据唯一性
- • 频繁的成员检测操作
- • 高性能的添加/删除操作
- • 集合运算功能
- • 简洁的 API
选择 Array 当你需要:
- • 通过索引访问元素
- • 使用丰富的数组方法
- • 保持重复值
- • 复杂的数据处理和转换
- • 更好的生态系统支持
在现代 JavaScript 开发中,理解这两种数据结构的特点并合理选择,能够显著提升代码的性能和可维护性。很多时候,最佳方案是根据不同的使用场景组合使用这两种数据结构。