为什么JavaScript的Set比Array更适合某些场景

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 开发中,理解这两种数据结构的特点并合理选择,能够显著提升代码的性能和可维护性。很多时候,最佳方案是根据不同的使用场景组合使用这两种数据结构。

相关推荐
EstherNi11 分钟前
vue3仿照elementui样式的写法,并进行校验,并且有默认值的设置
javascript·elementui
BumBle15 分钟前
Vue项目中实现路由守卫自动取消Pending请求
前端
gCode Teacher 格码致知22 分钟前
Javascript提高:get和post等请求,对于汉字和空格信息进行编码的原则-由Deepseek产生
开发语言·前端·javascript·node.js·jquery
竹林81823 分钟前
从ethers.js迁移到Viem:我在一个DeFi项目前端重构中踩过的坑
前端·javascript
像我这样帅的人丶你还44 分钟前
从交稿到甩锅预防:AI 前端流水线
前端·ai编程
想想弹幕会怎么做1 小时前
如何构建一颗可交互的ui树?
前端
程序员陆业聪1 小时前
我见过的最反直觉的 Android 架构问题:UseCase 越多,项目越烂
前端
Arya_aa1 小时前
网络:前端向后端发送网络请求渲染在页面上,将EasyMock中的信息用前端vue框架编写代码,最终展示在浏览器
前端·vue.js
LlNingyu1 小时前
文艺复兴,什么是CSRF,常见形式(一)
前端·安全·web安全·csrf
晓13131 小时前
React篇——第三章 状态管理之 Redux 篇
前端·javascript·react.js