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

相关推荐
上单带刀不带妹2 分钟前
Vue3递归组件详解:构建动态树形结构的终极方案
前端·javascript·vue.js·前端框架
-半.4 分钟前
Collection接口的详细介绍以及底层原理——包括数据结构红黑树、二叉树等,从0到彻底掌握Collection只需这篇文章
前端·html
90后的晨仔24 分钟前
📦 Vue CLI 项目结构超详细注释版解析
前端·vue.js
@大迁世界24 分钟前
用CSS轻松调整图片大小,避免拉伸和变形
前端·css
一颗不甘坠落的流星25 分钟前
【JS】获取元素宽高(例如div)
前端·javascript·react.js
白开水都有人用26 分钟前
VUE目录结构详解
前端·javascript·vue.js
if时光重来35 分钟前
axios统一封装规范管理
前端·vue.js
m0dw43 分钟前
js迭代器
开发语言·前端·javascript
烛阴1 小时前
别再让 JavaScript 卡死页面!Web Workers 零基础上手指南
前端·javascript
tianzhiyi1989sq1 小时前
Vue项目中的AJAX请求与跨域问题解析
前端·vue.js·ajax