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

205 阅读6分钟

JavaScript 提供了 Array 和 Set 来存储数据集合,虽然两者都能存储多个值,但在特定场景下,Set 具有显著的优势。让我们深入分析这两种数据结构的区别。

1. 数据唯一性:Set 的核心优势

1.1 重复值处理

Array(数组)

数组允许存储重复元素,这在某些场景下可能导致数据冗余:

const userIds = [1223334];
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 自动确保数据唯一性,无需额外处理:

const userIds = new Set([1223334]);
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 能正确处理特殊值的唯一性:

const specialValues = new Set([NaNNaN0, -0undefinednull]);
console.log(specialValues); // Set(4) {NaN, 0, undefined, null}
// 注意:Set 认为 NaN === NaN,0 === -0

2. 性能对比:查找和删除操作

2.1 查找性能

Array 的查找

const largeArray = Array.from({length100000}, (_, 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 的查找

const largeSet = new Set(Array.from({length100000}, (_, i) => i));

// 时间复杂度 O(1)
console.time('Set has');
const exists = largeSet.has(99999);
console.timeEnd('Set has'); // 更快

2.2 删除性能

Array 的删除

const arr = [12345];

// 删除特定值需要先找到索引,然后删除
const index = arr.indexOf(3);
if (index > -1) {
    arr.splice(index, 1); // O(n) 时间复杂度
}
console.log(arr); // [1, 2, 4, 5]

Set 的删除

const set = new Set([12345]);

// 直接删除,O(1) 时间复杂度
set.delete(3);
console.log(set); // Set(4) {1, 2, 4, 5}

3. API 设计:简洁性对比

3.1 基本操作

Array 的操作

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 的操作

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 支持链式调用,代码更简洁:

const result = new Set()
    .add(1)
    .add(2)
    .add(3);

console.log(result); // Set(3) {1, 2, 3}

4. 迭代和遍历

4.1 迭代方式对比

Array 的迭代

const arr = [12345];

// 多种迭代方式
arr.forEach((item, index) => console.log(item, index));
for (let i0; 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 的迭代

const setnew Set([12345]);

// 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 的顺序保证更可靠:

const setnew Set();
set.add('first');
set.add('second');
set.add('third');

console.log([...set]); // ['first', 'second', 'third']

5. 实际应用场景

5.1 数据去重

// 数组去重 - Set 方案
function removeDuplicates(arr) {
    return [...new Set(arr)];
}

const duplicates = [12233445];
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 集合运算

const setA = new Set([1234]);
const setB = new Set([3456]);

// 并集
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 权限和标签管理

// 用户权限管理
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 缓存和记录访问

// 访问记录管理
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. 性能基准测试

// 性能测试函数
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自动去重,避免重复数据
频繁的成员检测SetO(1) 时间复杂度,性能优异
需要索引访问Array支持通过索引直接访问元素
需要数组方法(map、filter等)Array丰富的内置方法
集合运算(交集、并集等)Set天然支持集合操作
需要存储重复值ArraySet 会自动去重
权限管理Set快速检查权限存在性
标签系统Set避免重复标签,快速查找
访问记录Set自动去重,快速检查
数据处理和转换Array更多的处理方法

8. 最佳实践建议

8.1 选择 Set 的情况

// ✅ 好的使用场景

// 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 的情况

// ✅ 好的使用场景

// 1. 需要索引访问
const items = ['first''second''third'];
console.log(items[1]); // 'second'

// 2. 需要数组方法
const numbers = [12345];
const doubled = numbers.map(x => x * 2);
const sum = numbers.reduce((a, b) => a + b, 0);

// 3. 需要保持重复值
const scores = [8590859285]; // 允许重复分数

// 4. 复杂数据处理
const users = [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}];
const names = users.map(user => user.name);

8.3 混合使用

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