JavaScript 的 Set 对象是 ES6 (ECMAScript 2015) 引入的一种新的内置对象,它提供了一种在集合中存储唯一值的机制。虽然它在概念上类似于数组,但其独有的特性使其在处理无重复数据和执行集合操作时异常强大和高效。理解 Set 的这些“趣闻”不仅能帮助您写出更简洁的代码,还能优化程序的性能。

本指南将带您深入探索 JavaScript Set 对象的奥秘,揭示其背后的核心特性和那些常常令人感到“有趣”或“意外”的事实。我们将从 Set 的基本概念、如何创建和操作它开始,详细解析其“唯一值”的判断机制(包括对引用类型的处理),探索它与数组之间的转换,以及它在性能优化和算法中的应用。掌握这些“趣闻”将使您对 Set 有更深刻的理解,从而成为一名更具洞察力的 JavaScript 开发者。


前置知识

为了更好地理解本指南,建议您具备以下基础知识:

  • JavaScript 基础语法: 了解变量声明(const, let)、基本数据类型、对象。
  • 数组 (Array): 熟悉数组的基本操作和概念。
  • 循环与迭代器: 了解 for...of 循环和可迭代对象的基本概念。

目录

  1. 引言:为什么我们需要 Set
  2. Set 的核心概念与创建
    • 2.1 Set 是什么?—— 唯一值的集合
    • 2.2 创建 Set 对象
      • Set
      • 从可迭代对象创建 (Array, String, Map 等)
    • 2.3 Set 的大小:size 属性
  3. Set 的核心操作方法
    • 3.1 add():添加元素
    • 3.2 delete():删除元素
    • 3.3 has():检查元素是否存在
    • 3.4 clear():清空 Set
  4. Set 的“趣闻”一:唯一值的判断机制
    • 4.1 严格相等 (===) 的细微之处
      • 基本类型值的唯一性
      • NaN 的特殊处理 (这是与 === 的一个“趣闻”差异)
      • +0-0 的处理
    • 4.2 引用类型值的唯一性
      • 对象和数组作为元素:按引用比较的“趣闻”
      • 每次添加新对象都认为是唯一的
  5. Set 的“趣闻”二:可迭代性与与数组的互操作
    • 5.1 Set 是一个可迭代对象 (Iterable)
      • 使用 for...of 遍历
      • keys(), values(), entries() 方法
    • 5.2 Set 与数组的转换
      • 数组转 Set (去重利器)
      • Set 转数组 (使用扩展运算符 ...Array.from())
  6. Set 的“趣闻”三:性能优势与应用场景
    • 6.1 has() 方法的性能优势 (特别是对于大量数据)
      • 与数组 includes() 的比较
    • 6.2 常见应用场景:
      • 数组去重 (最常见的用途)
      • 判断元素是否存在 (高效查找)
      • 实现集合操作 (交集、并集、差集等)
      • 追踪唯一 ID 或状态
  7. Set 的其他细节
    • 7.1 Set 只能存储值,没有键 (与 Map 的区别)
    • 7.2 元素的顺序 (插入顺序在某些引擎中保持,但不可依赖)
  8. 总结
  9. 附录:常见问题

1. 引言:为什么我们需要 Set

在 ES6 之前,JavaScript 开发者如果需要处理一组不重复的数据,通常会使用数组,并通过遍历或创建临时对象来手动实现去重和查找逻辑。这不仅代码冗长,而且对于大量数据而言,效率也往往不高。Set 对象的引入,正是为了原生、高效地解决这些问题。它提供了一种直观、优化的方式来管理唯一值的集合,极大地简化了相关操作。


2. Set 的核心概念与创建

2.1 Set 是什么?—— 唯一值的集合
  • Set 是一个集合数据结构,它存储的值是唯一的。
  • 它类似于数学中的集合,每个元素只能出现一次。
  • Set 可以存储任何类型的 JavaScript 值,包括原始值和对象引用。
2.2 创建 Set 对象
Set

使用 new Set() 构造函数可以创建一个空的 Set

const mySet = new Set();
console.log(mySet); // Set(0) {}
从可迭代对象创建 (Array, String, Map 等)

Set 构造函数可以接受一个可迭代对象作为参数,其中的所有元素都会被添加到新的 Set 中。重复的元素会被自动去除。

// 从数组创建 Set (自动去重)
const numbers = [1, 2, 3, 2, 1, 4];
const uniqueNumbers = new Set(numbers);
console.log(uniqueNumbers); // Set(4) { 1, 2, 3, 4 }

// 从字符串创建 Set (每个字符都是一个元素)
const greeting = "hello";
const uniqueChars = new Set(greeting);
console.log(uniqueChars); // Set(4) { 'h', 'e', 'l', 'o' } (l 重复,只保留一个)

// 从 Map 的 entries 创建 Set (每个 entry 都是一个数组 [key, value])
const myMap = new Map([
  ['a', 1],
  ['b', 2]
]);
const mapEntriesSet = new Set(myMap.entries());
console.log(mapEntriesSet); // Set(2) { ['a', 1], ['b', 2] }
2.3 Set 的大小:size 属性
  • Set 对象使用 size 属性来表示其包含元素的数量,而不是 length (数组使用 length)。
const mySet = new Set([1, 2, 3]);
console.log(mySet.size); // 3

3. Set 的核心操作方法

3.1 add():添加元素
  • Set 中添加一个新元素。如果元素已存在,则 Set 不会改变,但 add() 方法仍然会返回 Set 自身。
  • 支持链式调用。
const fruits = new Set();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Apple"); // 再次添加 "Apple",Set 不会改变

console.log(fruits); // Set(2) { 'Apple', 'Banana' }
fruits.add("Orange").add("Grape"); // 链式调用
console.log(fruits); // Set(4) { 'Apple', 'Banana', 'Orange', 'Grape' }
3.2 delete():删除元素
  • Set 中删除指定元素。
  • 如果元素存在并被成功删除,返回 true;如果元素不存在,返回 false
const colors = new Set(["Red", "Green", "Blue"]);
console.log(colors.delete("Green")); // true (Green 被删除)
console.log(colors);                  // Set(2) { 'Red', 'Blue' }
console.log(colors.delete("Yellow")); // false (Yellow 不存在)
3.3 has():检查元素是否存在
  • 检查 Set 中是否包含某个元素。
  • 如果存在,返回 true;否则返回 false
const names = new Set(["Alice", "Bob"]);
console.log(names.has("Alice")); // true
console.log(names.has("Charlie")); // false
3.4 clear():清空 Set
  • 移除 Set 中的所有元素。
const nums = new Set([1, 2, 3]);
nums.clear();
console.log(nums); // Set(0) {}

4. Set 的“趣闻”一:唯一值的判断机制

Set 的核心是存储唯一值,但它如何判断两个值是否“唯一”呢?它使用的是一种被称为 “值相等 (SameValueZero)” 的算法,这与严格相等 (===) 略有不同,特别是体现在 NaN 的处理上。

4.1 严格相等 (===) 的细微之处
  • 基本类型值的唯一性:

    • 对于原始值(number, string, boolean, undefined, null, symbol, bigint),Set 会像 === 一样进行比较。
    const s1 = new Set([1, "1", true, false, null, undefined, Symbol('a'), 1n]);
    s1.add(1);       // 已存在
    s1.add("1");     // 已存在
    s1.add(Symbol('a')); // 新的 Symbol 始终是唯一的
    console.log(s1); // Set(8) { 1, '1', true, false, null, undefined, Symbol(a), 1n, Symbol(a) }
    
  • NaN 的特殊处理 (这是与 === 的一个“趣闻”差异)

    • 在 JavaScript 中,NaN === NaN 的结果是 false
    • 然而,Set 在判断唯一性时,NaN 视为与自身相等。这意味着,您可以将 NaN 添加到 Set 中,但只能添加一次。
    const s2 = new Set();
    s2.add(NaN);
    s2.add(NaN); // 再次添加 NaN,不会增加 Set 的大小
    console.log(s2);     // Set(1) { NaN }
    console.log(s2.has(NaN)); // true
    

    这个特性非常“有趣”,因为它纠正了 NaN === NaN 这个“缺陷”,使得 NaN 在集合中 behave 得更符合直觉。

  • +0-0 的处理

    • Set+0-0 视为相等。
    const s3 = new Set();
    s3.add(0);
    s3.add(-0);
    console.log(s3); // Set(1) { 0 }
    
4.2 引用类型值的唯一性
  • 对象和数组作为元素:按引用比较的“趣闻”
    • 对于对象(包括普通对象 {}, 数组 [], 函数 function() 等),Set 会根据引用地址来判断唯一性。
    • 只有当两个变量指向内存中的同一个对象时,它们才被 Set 认为是相同的。
    • 即使两个对象具有完全相同的属性和值,只要它们是不同的对象实例,Set 也会将它们视为唯一的。
    const obj1 = { id: 1 };
    const obj2 = { id: 1 };
    const arr1 = [1, 2];
    const arr2 = [1, 2];
    const func1 = () => {};
    const func2 = () => {};
    
    const s4 = new Set();
    s4.add(obj1);
    s4.add(obj2);     // obj2 是一个新的对象引用,所以被视为唯一
    s4.add(arr1);
    s4.add(arr2);     // arr2 是一个新的数组引用,所以被视为唯一
    s4.add(func1);
    s4.add(func2);    // func2 是一个新的函数引用,所以被视为唯一
    
    console.log(s4.size); // 6
    
    s4.add(obj1); // 再次添加 obj1,Set 不变 (因为是同一个引用)
    console.log(s4.size); // 6
    
    这个“趣闻”强调了 JavaScript 中引用类型和原始类型在比较时的根本区别。如果你想要根据对象的内容去重,你需要自己实现比较逻辑,或者将对象序列化为字符串再存储。

5. Set 的“趣闻”二:可迭代性与与数组的互操作

Set 对象是 JavaScript 中的一个可迭代对象,这意味着它可以使用所有支持迭代器协议的语法(如 for...of 循环、扩展运算符 ...)。

5.1 Set 是一个可迭代对象 (Iterable)
  • 使用 for...of 遍历:
    const colors = new Set(["Red", "Green", "Blue"]);
    for (const color of colors) {
      console.log(color);
    }
    // 输出: Red, Green, Blue
    
  • keys(), values(), entries() 方法:
    • Set.prototype.values():返回一个包含 Set 中所有值的迭代器。
    • Set.prototype.keys():与 values() 行为相同,因为 Set 没有键,它返回的迭代器中,每个值都是其自身。
    • Set.prototype.entries():返回一个包含 [value, value] 数组对的迭代器。
    const mySet = new Set([1, 2]);
    console.log(mySet.values());  // SetIterator {1, 2}
    console.log(mySet.keys());    // SetIterator {1, 2}
    console.log(mySet.entries()); // SetIterator { [1, 1], [2, 2] }
    
5.2 Set 与数组的转换

这是 Set 最常用也最实用的功能之一。

  • 数组转 Set (去重利器)
    • 通过 new Set(array) 构造函数轻松实现数组去重。
    const numbers = [1, 2, 3, 2, 1, 4];
    const uniqueNumbersSet = new Set(numbers);
    console.log(uniqueNumbersSet); // Set(4) { 1, 2, 3, 4 }
    
  • Set 转数组 (使用扩展运算符 ...Array.from())
    • 利用 Set 的可迭代性,结合扩展运算符 ...Array.from() 可以方便地将 Set 转换回数组。
    const uniqueNums = new Set([1, 2, 3, 4]);
    const uniqueNumsArray = [...uniqueNums]; // 使用扩展运算符
    console.log(uniqueNumsArray);           // [1, 2, 3, 4]
    
    const uniqueCharsArray = Array.from(new Set("hello")); // 使用 Array.from()
    console.log(uniqueCharsArray);          // ['h', 'e', 'l', 'o']
    
    这种转换方式提供了一种简洁高效的数组去重方案:[...new Set(array)]

6. Set 的“趣闻”三:性能优势与应用场景

Set 不仅仅是语法糖,它在某些场景下提供了显著的性能优势。

6.1 has() 方法的性能优势 (特别是对于大量数据)
  • Set 内部通常通过哈希表(或类似的结构)实现,这意味着 add(), delete(), has() 方法的平均时间复杂度接近 O(1) (常数时间)。
  • 相比之下,数组的 includes() 方法需要遍历整个数组,其时间复杂度为 O(n) (线性时间)。对于大型数据集,Set.has() 的性能优势非常明显。
    const largeArray = Array.from({ length: 100000 }, (_, i) => i);
    const largeSet = new Set(largeArray);
    
    console.time("Array includes");
    largeArray.includes(99999); // O(n)
    console.timeEnd("Array includes"); // 耗时可能几十微秒
    
    console.time("Set has");
    largeSet.has(99999); // O(1)
    console.timeEnd("Set has");     // 耗时通常在几微秒以下
    
6.2 常见应用场景:
  • 数组去重 (最常见的用途): [...new Set(array)] 已经成为 JavaScript 中最简洁高效的数组去重方案。
  • 判断元素是否存在 (高效查找): 当需要频繁检查一个元素是否在集合中时,Set.has() 提供了极高的效率。
  • 实现集合操作 (交集、并集、差集等): Set 可以作为实现这些数学集合操作的基础。
    const setA = new Set([1, 2, 3, 4]);
    const setB = new Set([3, 4, 5, 6]);
    
    // 并集 (Union): 所有元素的集合
    const union = new Set([...setA, ...setB]); // Set {1, 2, 3, 4, 5, 6}
    
    // 交集 (Intersection): 共同元素的集合
    const intersection = new Set([...setA].filter(x => setB.has(x))); // Set {3, 4}
    
    // 差集 (Difference): A 有而 B 没有的元素
    const difference = new Set([...setA].filter(x => !setB.has(x))); // Set {1, 2}
    
  • 追踪唯一 ID 或状态: 在需要确保每个 ID 或状态只被记录一次的场景中非常有用。

7. Set 的其他细节

7.1 Set 只能存储值,没有键 (与 Map 的区别)
  • Set 存储的是一系列值,它没有与这些值关联的键。
  • 如果您需要存储键值对,应该使用 Map 对象。
7.2 元素的顺序 (插入顺序在某些引擎中保持,但不可依赖)
  • 根据 ES6 规范,Set 的迭代顺序是其元素的插入顺序
  • 虽然大多数现代 JavaScript 引擎都遵循这个规范,但在一些旧环境或特定场景下,不应完全依赖 Set 元素的顺序。如果你需要严格的顺序,最好将 Set 转换为数组并对其进行排序。

8. 总结

JavaScript Set 对象是 ES6 带来的一项强大且实用的特性。它的核心“趣闻”在于:

  • 只存储唯一值: 通过其独特的“值相等 (SameValueZero)”算法实现,甚至将 NaN 视为与自身相等。
  • 引用类型按引用比较: 强调了对象比较的本质。
  • 高度可迭代: 方便与数组进行双向转换,特别是作为数组去重和查找的利器。
  • 卓越的性能: has() 方法在处理大量数据时提供 O(1) 的平均时间复杂度,远优于数组的 includes()

深入理解这些“趣闻”和核心特性,将使您能够更有效地利用 Set 对象,编写出更简洁、更高效、更符合语义的 JavaScript 代码,从而解决现实世界中的各种数据处理问题。


9. 附录:常见问题

Q: SetArray 在去重方面有什么区别?为什么 Set 更高效?
A:

  • Array 去重: 通常需要遍历数组,并使用 indexOf()filter()reduce() 或创建辅助对象等方法。这些方法的时间复杂度通常为 O(n^2) (如 indexOf) 或 O(n) (如使用对象哈希表),但实现相对复杂。
  • Set 去重: [...new Set(array)] 是最简洁高效的方式。Set 内部使用哈希表,在添加元素时,判断唯一性是 O(1) 的平均时间复杂度。因此,整个去重过程的平均时间复杂度接近 O(n)。
  • 效率: 对于小型数组,两者差异不明显。但对于大型数组,Set 的性能优势非常显著。

Q: SetMap 有什么关系和区别?
A:

  • 关系: 它们都是 ES6 引入的新的集合类型,都提供了更高效的数据存储和检索机制,并且都支持迭代器协议。它们在内部实现上都可能依赖哈希表。
  • 区别:
    • Set 存储唯一的值。每个元素都是一个独立的值,没有与之关联的键。
    • Map 存储键值对。每个元素都是一个 [key, value] 对,key 是唯一的。
    • 用途: Set 主要用于去重、判断元素是否存在。Map 主要用于存储关联数据、作为自定义对象键的字典。

Q: Set 能保持元素的插入顺序吗?
A: 根据 ES6 规范,Set 的迭代顺序是其元素的插入顺序。这意味着当你遍历 Set 时,元素的返回顺序与它们被添加到 Set 中的顺序一致。然而,虽然这是规范规定的行为,但在某些旧环境或特殊场景下,不应过于依赖此特性。如果严格的顺序对您的应用程序至关重要,建议将 Set 转换为数组后进行排序或使用其他数据结构。

Q: Set 中可以存储对象吗?如何判断对象是否唯一?
A: Set 可以存储对象。但是,它通过引用地址来判断对象的唯一性。这意味着:

  • 如果你添加同一个对象的多次引用,Set 只会存储一次。
  • 如果你添加两个内容完全相同但却是不同内存地址的对象,Set 会将它们视为两个不同的唯一元素。
    const obj = { id: 1 };
    const s = new Set();
    s.add(obj);
    s.add(obj); // 相同引用,Set 不变
    s.add({ id: 1 }); // 不同引用,视为新元素
    console.log(s.size); // 2
    
    如果你需要根据对象的内容而不是引用来判断唯一性,你需要自己实现逻辑(例如,将对象转换为 JSON 字符串来存储,但这样会丢失原始对象类型)。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐