JavaScript 中的集合论:Set 对象的数学特性与操作

JavaScript 中的集合论:Set 对象的数学特性与操作

各位编程爱好者、专家们,大家好。今天我们深入探讨一个在现代JavaScript开发中日益重要且功能强大的数据结构——Set 对象。它不仅仅是一个存储数据的容器,更是JavaScript对数学中“集合”概念的直接映射与实现。理解Set的数学特性,能够帮助我们更优雅、高效地处理数据,并编写出逻辑更严谨、意图更清晰的代码。

1. 数学集合的基础与JavaScript Set 的引入

在数学中,集合是一个由互不相同(distinct)的元素组成的整体,这些元素之间没有固定的顺序。集合论是数学的一个基本分支,它为我们处理和组织数据提供了强大的抽象工具。

JavaScript中的 Set 对象正是对这一数学概念的直接实现。它允许你存储任何类型(无论是原始值还是对象引用)的唯一值。

主要特性:

  • 唯一性 (Uniqueness): Set 中的每个值都必须是唯一的。如果尝试添加一个已经存在于 Set 中的值,它将被忽略,Set 不会改变。
  • 无序性 (Unordered): Set 中的元素没有特定的顺序。虽然在某些JavaScript引擎的实现中,迭代顺序可能看起来与插入顺序一致,但我们不应依赖这种行为。
  • 混合类型 (Mixed Types): Set 可以存储不同数据类型的值,例如数字、字符串、布尔值,甚至是对象和函数。

Set 对象提供了高效的添加、删除、检查元素以及迭代操作,这使得它在需要处理唯一元素集合的场景中表现出色。

2. Set 对象的创建与基本操作

我们首先从 Set 对象的创建和其核心操作开始。

2.1 创建 Set

Set 构造函数可以接受一个可选的 Iterable 对象(如数组、字符串等)作为参数,用于初始化 Set

// 1. 创建一个空的 Set
const myEmptySet = new Set();
console.log(myEmptySet); // Set(0) {}

// 2. 使用数组初始化 Set,重复元素会被自动去重
const numbers = [1, 2, 3, 2, 1, 4, 5];
const uniqueNumbers = new Set(numbers);
console.log(uniqueNumbers); // Set(5) { 1, 2, 3, 4, 5 }

// 3. 使用字符串初始化 Set (字符串是可迭代的,每个字符被视为一个元素)
const greeting = "hello";
const uniqueChars = new Set(greeting);
console.log(uniqueChars); // Set(4) { 'h', 'e', 'l', 'o' }

// 4. 使用 Set 初始化另一个 Set (通常没必要,除非要复制)
const setA = new Set([1, 2, 3]);
const setB = new Set(setA);
console.log(setB); // Set(3) { 1, 2, 3 }

2.2 添加元素 (add())

add() 方法用于向 Set 中添加新元素。如果元素已存在,Set 不会改变。add() 方法返回 Set 实例本身,这允许进行链式调用。

const mySet = new Set();

mySet.add(1);
mySet.add('hello');
mySet.add({ name: 'Alice' }); // 添加一个对象引用

console.log(mySet); // Set(3) { 1, 'hello', { name: 'Alice' } }

mySet.add(1); // 尝试添加重复元素,Set 不变
console.log(mySet); // Set(3) { 1, 'hello', { name: 'Alice' } }

// 链式调用
mySet.add(true).add(null);
console.log(mySet); // Set(5) { 1, 'hello', { name: 'Alice' }, true, null }

2.3 检查元素是否存在 (has())

has() 方法用于检查 Set 中是否包含某个元素,返回一个布尔值。这是一个非常高效的操作,平均时间复杂度为 O(1)。

const fruits = new Set(['apple', 'banana', 'orange']);

console.log(fruits.has('apple'));    // true
console.log(fruits.has('grape'));    // false

const obj = { id: 1 };
fruits.add(obj);
console.log(fruits.has(obj));        // true

// 注意:对于对象,has() 检查的是引用是否相同
console.log(fruits.has({ id: 1 }));  // false (这是另一个不同的对象引用)

2.4 删除元素 (delete())

delete() 方法用于从 Set 中移除指定元素。如果元素存在并被成功移除,返回 true;如果元素不存在,返回 false

const colors = new Set(['red', 'green', 'blue']);

console.log(colors.delete('green'));  // true (成功删除)
console.log(colors);                  // Set(2) { 'red', 'blue' }

console.log(colors.delete('yellow')); // false (元素不存在)
console.log(colors);                  // Set(2) { 'red', 'blue' }

2.5 获取 Set 的大小 (size)

size 属性返回 Set 中元素的数量。

const items = new Set([10, 20, 30]);
console.log(items.size); // 3

items.add(40);
console.log(items.size); // 4

items.delete(10);
console.log(items.size); // 3

2.6 清空 Set (clear())

clear() 方法会移除 Set 中的所有元素,使其变为空 Set

const data = new Set([1, 2, 3, 'a', 'b']);
console.log(data);      // Set(5) { 1, 2, 3, 'a', 'b' }
console.log(data.size); // 5

data.clear();
console.log(data);      // Set(0) {}
console.log(data.size); // 0

2.7 元素唯一性的判断规则

Set 在判断元素唯一性时,遵循“Same-value-zero”算法。这个算法与严格相等运算符 === 有一些关键区别:

  • NaN (Not-a-Number):Set 中,NaN 被视为与自身相等(NaN === NaNfalse,但 Set 认为它们相同)。因此,一个 Set 中最多只能有一个 NaN
  • +0 和 -0:Set 中,+0-0 被视为相等。
  • 对象 (Objects): Set 通过引用来判断对象的唯一性。这意味着两个内容相同的对象字面量 {} 会被视为不同的对象,因为它们是不同的内存引用。
const trickySet = new Set();

trickySet.add(NaN);
trickySet.add(NaN); // 尝试添加第二个 NaN,无效
console.log(trickySet); // Set(1) { NaN }

trickySet.add(0);
trickySet.add(-0); // 尝试添加 -0,无效 (与 0 视为相等)
console.log(trickySet); // Set(2) { NaN, 0 }

const obj1 = { id: 1 };
const obj2 = { id: 1 };
const obj3 = obj1; // obj3 引用了 obj1

trickySet.add(obj1);
trickySet.add(obj2); // obj2 是不同的引用,所以被添加
trickySet.add(obj3); // obj3 和 obj1 是相同的引用,无效
console.log(trickySet); // Set(4) { NaN, 0, { id: 1 }, { id: 1 } }

理解这些规则对于正确使用 Set 至关重要,尤其是在处理涉及 NaN 或比较对象引用的场景。

3. Set 对象的迭代

Set 对象是可迭代的(Iterable),这意味着你可以使用 for...of 循环、forEach() 方法以及 Set.prototype.values()keys()entries() 方法来遍历其元素。

3.1 使用 for...of 循环

这是最直接和推荐的迭代方式。

const fruits = new Set(['apple', 'banana', 'orange']);

for (const fruit of fruits) {
    console.log(fruit);
}
// 输出:
// apple
// banana
// orange

3.2 使用 forEach() 方法

forEach() 方法接受一个回调函数,该函数会为 Set 中的每个元素执行一次。回调函数有三个参数:value(当前元素的值)、key(在 Set 中,keyvalue 是相同的)、set(当前正在遍历的 Set 对象)。

const colors = new Set(['red', 'green', 'blue']);

colors.forEach((value, key, set) => {
    console.log(`Value: ${value}, Key: ${key}`); // 在 Set 中,value 和 key 是相同的
    console.log(set === colors); // true
});
// 输出:
// Value: red, Key: red
// true
// Value: green, Key: green
// true
// Value: blue, Key: blue
// true

3.3 values(), keys(), entries() 方法

  • Set.prototype.values(): 返回一个新的迭代器对象,其中包含 Set 中所有元素的值。
  • Set.prototype.keys(): 返回一个新的迭代器对象,其中包含 Set 中所有元素的值。在 Set 中,keys()values() 是相同的,因为 Set 没有键。
  • Set.prototype.entries(): 返回一个新的迭代器对象,其中包含 Set 中所有元素的 [value, value] 对。这主要是为了与 Map 对象的 entries() 方法保持API一致性。
const items = new Set(['A', 'B', 'C']);

// values()
const valuesIterator = items.values();
console.log(valuesIterator.next().value); // A
console.log(valuesIterator.next().value); // B

// keys() (与 values() 相同)
const keysIterator = items.keys();
console.log(keysIterator.next().value);   // A

// entries()
const entriesIterator = items.entries();
console.log(entriesIterator.next().value); // ['A', 'A']
console.log(entriesIterator.next().value); // ['B', 'B']

// 转换为数组
const itemsArray = [...items]; // 使用展开运算符
console.log(itemsArray); // ['A', 'B', 'C']

const valuesArray = Array.from(items.values());
console.log(valuesArray); // ['A', 'B', 'C']

4. 基于 Set 实现数学集合操作

Set 对象在JavaScript中提供了实现标准数学集合操作的绝佳基础。虽然 Set 本身并没有直接提供如“并集”、“交集”等方法,但我们可以利用其现有方法轻松构建这些操作。

我们将探讨以下核心集合操作:

  • 并集 (Union)
  • 交集 (Intersection)
  • 差集 (Difference / Relative Complement)
  • 对称差集 (Symmetric Difference)
  • 子集 (Subset)
  • 超集 (Superset)
  • 不相交集 (Disjoint Sets)

为了方便演示和重用,我们将这些操作封装为函数。

4.1 并集 (Union)

数学定义: 集合 A 和 B 的并集是一个新集合,包含 A 或 B 中的所有元素,不含重复元素。记作 $A cup B$。

实现思路: 创建一个新 Set,将集合 A 的所有元素添加进去,然后将集合 B 的所有元素也添加进去。Set 的唯一性特性会自动处理重复元素。

/**
 * 计算两个 Set 的并集。
 * @param {Set} set1 第一个集合。
 * @param {Set} set2 第二个集合。
 * @returns {Set} 包含两个集合所有唯一元素的新 Set。
 */
function union(set1, set2) {
    const result = new Set(set1); // 以 set1 的元素初始化
    for (const item of set2) {
        result.add(item); // 添加 set2 的元素,Set 会自动去重
    }
    return result;

    // 更简洁的方式:
    // return new Set([...set1, ...set2]);
}

const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);
const unionSet = union(setA, setB);
console.log("并集 (A ∪ B):", unionSet); // Set(5) { 1, 2, 3, 4, 5 }

const setC = new Set(['apple', 'banana']);
const setD = new Set(['banana', 'orange', 'grape']);
const unionSet2 = union(setC, setD);
console.log("并集 (C ∪ D):", unionSet2); // Set(4) { 'apple', 'banana', 'orange', 'grape' }

4.2 交集 (Intersection)

数学定义: 集合 A 和 B 的交集是一个新集合,包含同时属于 A 和 B 的所有元素。记作 $A cap B$。

实现思路: 遍历一个集合的所有元素,检查每个元素是否存在于另一个集合中。如果存在,则将其添加到结果 Set 中。

/**
 * 计算两个 Set 的交集。
 * @param {Set} set1 第一个集合。
 * @param {Set} set2 第二个集合。
 * @returns {Set} 包含同时属于两个集合的元素的新 Set。
 */
function intersection(set1, set2) {
    const result = new Set();
    // 遍历较小的集合通常更高效
    const [smallerSet, largerSet] = set1.size < set2.size ? [set1, set2] : [set2, set1];

    for (const item of smallerSet) {
        if (largerSet.has(item)) {
            result.add(item);
        }
    }
    return result;

    // 另一种使用 filter 的方式,但通常循环更高效
    // return new Set([...set1].filter(item => set2.has(item)));
}

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
const intersectionSet = intersection(setA, setB);
console.log("交集 (A ∩ B):", intersectionSet); // Set(2) { 3, 4 }

const setC = new Set(['apple', 'banana', 'kiwi']);
const setD = new Set(['banana', 'grape', 'kiwi']);
const intersectionSet2 = intersection(setC, setD);
console.log("交集 (C ∩ D):", intersectionSet2); // Set(2) { 'banana', 'kiwi' }

4.3 差集 (Difference / Relative Complement)

数学定义: 集合 A 对集合 B 的差集(或 A 相对于 B 的补集)是一个新集合,包含属于 A 但不属于 B 的所有元素。记作 $A setminus B$ 或 $A – B$。

实现思路: 遍历集合 A 的所有元素,如果某个元素不存在于集合 B 中,则将其添加到结果 Set 中。

/**
 * 计算 set1 对 set2 的差集 (set1 - set2)。
 * @param {Set} set1 第一个集合。
 * @param {Set} set2 第二个集合。
 * @returns {Set} 包含属于 set1 但不属于 set2 的元素的新 Set。
 */
function difference(set1, set2) {
    const result = new Set();
    for (const item of set1) {
        if (!set2.has(item)) {
            result.add(item);
        }
    }
    return result;

    // 另一种使用 filter 的方式:
    // return new Set([...set1].filter(item => !set2.has(item)));
}

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
const differenceSet = difference(setA, setB);
console.log("差集 (A - B):", differenceSet); // Set(2) { 1, 2 }

const setC = new Set(['apple', 'banana', 'kiwi']);
const setD = new Set(['banana', 'grape', 'kiwi']);
const differenceSet2 = difference(setC, setD);
console.log("差集 (C - D):", differenceSet2); // Set(1) { 'apple' }

4.4 对称差集 (Symmetric Difference)

数学定义: 集合 A 和 B 的对称差集是一个新集合,包含只属于 A 或只属于 B 的所有元素(即不属于两者交集的所有元素)。记作 $A Delta B$ 或 $A ominus B$。

实现思路: 可以通过两种方式实现:

  1. 计算 $(A setminus B) cup (B setminus A)$。
  2. 计算 $(A cup B) setminus (A cap B)$。

这里我们采用第一种方法,因为它直接使用了我们已经实现的 differenceunion 函数。

/**
 * 计算两个 Set 的对称差集。
 * @param {Set} set1 第一个集合。
 * @param {Set} set2 第二个集合。
 * @returns {Set} 包含只属于其中一个集合的元素的新 Set。
 */
function symmetricDifference(set1, set2) {
    const diff1 = difference(set1, set2); // 属于 set1 但不属于 set2
    const diff2 = difference(set2, set1); // 属于 set2 但不属于 set1
    return union(diff1, diff2); // 两者并集
}

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
const symmetricDifferenceSet = symmetricDifference(setA, setB);
console.log("对称差集 (A Δ B):", symmetricDifferenceSet); // Set(4) { 1, 2, 5, 6 }

const setC = new Set(['apple', 'banana', 'kiwi']);
const setD = new Set(['banana', 'grape', 'kiwi']);
const symmetricDifferenceSet2 = symmetricDifference(setC, setD);
console.log("对称差集 (C Δ D):", symmetricDifferenceSet2); // Set(2) { 'apple', 'grape' }

4.5 子集 (Subset)

数学定义: 如果集合 A 的所有元素都属于集合 B,则 A 是 B 的子集。记作 $A subseteq B$。

实现思路: 遍历集合 A 的所有元素,检查每个元素是否存在于集合 B 中。如果发现任何一个 A 的元素不在 B 中,则 A 不是 B 的子集。如果 A 的所有元素都在 B 中,则 A 是 B 的子集。
优化:如果 A 的大小大于 B 的大小,那么 A 肯定不是 B 的子集。

/**
 * 检查 set1 是否是 set2 的子集 (set1 ⊆ set2)。
 * @param {Set} set1 潜在的子集。
 * @param {Set} set2 潜在的超集。
 * @returns {boolean} 如果 set1 是 set2 的子集,则返回 true;否则返回 false。
 */
function isSubset(set1, set2) {
    if (set1.size > set2.size) {
        return false; // 子集的大小不能大于超集
    }
    for (const item of set1) {
        if (!set2.has(item)) {
            return false; // 发现 set1 中有元素不在 set2 中
        }
    }
    return true; // set1 的所有元素都在 set2 中
}

const setA = new Set([1, 2]);
const setB = new Set([1, 2, 3, 4]);
console.log("A 是 B 的子集?", isSubset(setA, setB)); // true

const setC = new Set([1, 5]);
const setD = new Set([1, 2, 3]);
console.log("C 是 D 的子集?", isSubset(setC, setD)); // false (因为 5 不在 D 中)

const emptySet = new Set();
console.log("空集是任何集合的子集?", isSubset(emptySet, setB)); // true
console.log("任何集合是自身的子集?", isSubset(setB, setB)); // true

4.6 超集 (Superset)

数学定义: 如果集合 B 的所有元素都属于集合 A,则 A 是 B 的超集。记作 $A supseteq B$。这等价于 B 是 A 的子集。

实现思路: 直接调用 isSubset(set2, set1)

/**
 * 检查 set1 是否是 set2 的超集 (set1 ⊇ set2)。
 * @param {Set} set1 潜在的超集。
 * @param {Set} set2 潜在的子集。
 * @returns {boolean} 如果 set1 是 set2 的超集,则返回 true;否则返回 false。
 */
function isSuperset(set1, set2) {
    return isSubset(set2, set1); // 超集判断就是反向的子集判断
}

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([1, 2]);
console.log("A 是 B 的超集?", isSuperset(setA, setB)); // true

const setC = new Set([1, 2]);
const setD = new Set([1, 2, 3]);
console.log("C 是 D 的超集?", isSuperset(setC, setD)); // false (因为 C 没有 3)

4.7 不相交集 (Disjoint Sets)

数学定义: 如果两个集合没有共同的元素(即它们的交集为空集),则称它们是不相交的。

实现思路: 计算两个集合的交集。如果交集的大小为 0,则它们是不相交的。

/**
 * 检查两个 Set 是否不相交。
 * @param {Set} set1 第一个集合。
 * @param {Set} set2 第二个集合。
 * @returns {boolean} 如果两个集合不相交,则返回 true;否则返回 false。
 */
function areDisjoint(set1, set2) {
    return intersection(set1, set2).size === 0;
}

const setA = new Set([1, 2, 3]);
const setB = new Set([4, 5, 6]);
console.log("A 和 B 是不相交集?", areDisjoint(setA, setB)); // true

const setC = new Set([1, 2, 3]);
const setD = new Set([3, 4, 5]);
console.log("C 和 D 是不相交集?", areDisjoint(setC, setD)); // false (因为有共同元素 3)

4.8 集合操作总结表

操作名称 数学符号 定义 JavaScript 实现思路
并集 $A cup B$ 包含 A 或 B 中的所有元素 new Set([...setA, ...setB])
交集 $A cap B$ 包含同时属于 A 和 B 的所有元素 遍历较小集合,检查元素是否在另一集合中 (setB.has(item))
差集 $A setminus B$ 包含属于 A 但不属于 B 的所有元素 遍历 setA,检查元素是否不在 setB 中 (!setB.has(item))
对称差集 $A Delta B$ 包含只属于 A 或只属于 B 的所有元素 union(difference(setA, setB), difference(setB, setA))
子集 $A subseteq B$ 如果 A 的所有元素都属于 B 遍历 setA,检查所有元素是否都在 setB 中
超集 $A supseteq B$ 如果 B 的所有元素都属于 A (等价于 B 是 A 的子集) isSubset(setB, setA)
不相交集 $A cap B = emptyset$ 如果 A 和 B 没有共同元素 intersection(setA, setB).size === 0

5. Set 的高级用法与性能考量

Set 不仅仅用于模拟数学集合,它在实际开发中还有许多高效且简洁的应用场景。

5.1 数组去重

这是 Set 最常见和最直接的用途之一。

const numbersWithDuplicates = [1, 2, 3, 2, 1, 4, 5, 5, 6];
const uniqueNumbersArray = [...new Set(numbersWithDuplicates)];
console.log(uniqueNumbersArray); // [1, 2, 3, 4, 5, 6]

const objectsWithDuplicates = [
    { id: 1, name: 'A' },
    { id: 2, name: 'B' },
    { id: 1, name: 'A' } // 引用不同,Set 无法直接去重
];
// 如果要基于对象内容去重,需要更复杂的逻辑,例如先将对象序列化为字符串
const uniqueObjects = new Set(objectsWithDuplicates);
console.log(uniqueObjects); // Set(3) { { id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 1, name: 'A' } }

// 基于特定属性值去重 (例如 id)
function uniqueObjectsById(arr) {
    const seenIds = new Set();
    return arr.filter(obj => {
        if (seenIds.has(obj.id)) {
            return false;
        } else {
            seenIds.add(obj.id);
            return true;
        }
    });
}
const uniqueObjs = uniqueObjectsById(objectsWithDuplicates);
console.log(uniqueObjs); // [ { id: 1, name: 'A' }, { id: 2, name: 'B' } ]

5.2 统计唯一元素数量

const data = ['apple', 'banana', 'apple', 'orange', 'banana', 'kiwi'];
const uniqueCount = new Set(data).size;
console.log("唯一元素数量:", uniqueCount); // 4

5.3 追踪已访问状态或缓存

在算法(如图遍历、递归深度优先搜索)中,Set 可以高效地存储已访问的节点或状态,防止重复计算或陷入无限循环。

function hasDuplicates(arr) {
    const seen = new Set();
    for (const item of arr) {
        if (seen.has(item)) {
            return true; // 发现重复
        }
        seen.add(item);
    }
    return false; // 没有重复
}

console.log(hasDuplicates([1, 2, 3, 4]));     // false
console.log(hasDuplicates([1, 2, 3, 2]));     // true

// 模拟一个简单的图遍历,避免重复访问节点
const graph = {
    A: ['B', 'C'],
    B: ['A', 'D'],
    C: ['A', 'E'],
    D: ['B'],
    E: ['C']
};

function bfs(startNode) {
    const visited = new Set();
    const queue = [startNode];
    visited.add(startNode);
    let path = [];

    while (queue.length > 0) {
        const node = queue.shift();
        path.push(node);

        for (const neighbor of graph[node]) {
            if (!visited.has(neighbor)) {
                visited.add(neighbor);
                queue.push(neighbor);
            }
        }
    }
    return path;
}

console.log("BFS 遍历路径:", bfs('A')); // BFS 遍历路径: [ 'A', 'B', 'C', 'D', 'E' ]

5.4 性能考量 (Set vs Array vs Object)

Set 在某些操作上比 ArrayObject 具有显著的性能优势。

  • has() / add() / delete() 操作: Set 在这些操作上平均时间复杂度为 O(1)。这是因为 Set 内部通常使用哈希表(Hash Table)或类似的结构来实现。

    • 对比 Array: Array.prototype.includes()Array.prototype.indexOf() 的时间复杂度为 O(n),因为它们可能需要遍历整个数组。对于大型数据集,Set 的优势非常明显。
    • 对比 Object: 如果将 Object 作为哈希表使用(例如 obj[key] = true),其 in 运算符或直接属性访问的平均时间复杂度也是 O(1)。然而,Set 语义上更明确地表示一个“值集合”,而不是“键值对映射”。当值本身就是你需要存储的唯一标识时,Set 更合适。
  • 内存使用: Set 通常比将数组转换为对象来模拟集合更节省内存,因为它不需要存储额外的键(在对象中,即使值是 true,也需要一个键)。

何时选择 Set

  • 你需要存储一组唯一的、无序的元素。
  • 你需要频繁地检查某个元素是否存在于集合中。
  • 你需要高效地添加或删除元素。
  • 你需要执行数学集合操作(并集、交集、差集等)。

何时选择 Array

  • 元素的顺序很重要。
  • 存在重复元素是允许的,或者需要保留重复元素。
  • 需要通过索引访问元素。

何时选择 Object

  • 你需要存储键值对,其中键是字符串或 Symbol。
  • 你需要通过键来查找值。

5.5 WeakSet 简介

JavaScript 还提供了 WeakSet 对象,它与 Set 有一些关键区别:

  • 只能存储对象引用: WeakSet 只能存储对象引用,不能存储原始值(如数字、字符串、布尔值)。
  • 弱引用: WeakSet 中的对象引用是“弱引用”。这意味着如果一个对象没有其他地方引用它,即使它存在于 WeakSet 中,垃圾回收器也可以将其回收。这对于避免内存泄漏非常有用。
  • 不可迭代: WeakSet 不可迭代,不能使用 for...of 循环或 forEach()。它也没有 size 属性。
  • API 有限: WeakSet 只有 add(), has(), delete() 三个方法。

WeakSet 主要用于跟踪对象实例,而不需要担心这些对象会因为存在于 WeakSet 中而阻止垃圾回收。例如,可以用来标记已处理过的对象,或者存储与特定对象关联的元数据,而不需要手动清理。

let obj1 = { id: 1 };
let obj2 = { id: 2 };

const ws = new WeakSet();
ws.add(obj1);
ws.add(obj2);

console.log(ws.has(obj1)); // true

obj1 = null; // 解除对 obj1 的强引用

// 此时,如果 obj1 没有其他强引用,它可能在未来的某个时刻被垃圾回收。
// WeakSet 不会阻止这种回收。
// 无法直接检查 ws 是否还包含 obj1,因为它不可迭代。
// 但如果再次创建 {id: 1},它与之前的 obj1 是不同的对象引用。

6. Set 在实际场景中的应用案例

6.1 管理用户权限或角色

假设我们有一个用户对象,其中包含其所属的角色列表,我们需要检查用户是否拥有执行某个操作所需的特定角色。

const userRoles = new Set(['admin', 'editor', 'reporter']);
const requiredRolesForPublish = new Set(['admin', 'editor']);
const requiredRolesForReview = new Set(['reviewer', 'editor']);

/**
 * 检查用户是否拥有所需权限中的任何一个。
 * @param {Set} userRoles 用户拥有的角色集合。
 * @param {Set} requiredRoles 需要的角色集合。
 * @returns {boolean} 如果用户拥有至少一个所需角色,则返回 true。
 */
function hasAnyRequiredRole(userRoles, requiredRoles) {
    // 遍历所需角色,检查用户是否拥有其中任何一个
    for (const role of requiredRoles) {
        if (userRoles.has(role)) {
            return true;
        }
    }
    return false;
    // 也可以使用交集判断:
    // return intersection(userRoles, requiredRoles).size > 0;
}

console.log("用户可以发布内容吗?", hasAnyRequiredRole(userRoles, requiredRolesForPublish)); // true (因为有 admin 和 editor)
console.log("用户可以审核内容吗?", hasAnyRequiredRole(userRoles, requiredRolesForReview)); // true (因为有 editor)

const userRoles2 = new Set(['reporter']);
console.log("用户2可以发布内容吗?", hasAnyRequiredRole(userRoles2, requiredRolesForPublish)); // false

6.2 过滤唯一访问者或 IP 地址

在 Web 服务器日志分析中,Set 可以用来快速统计独立访问者数量,或识别唯一的 IP 地址。

const accessLogs = [
    '192.168.1.1',
    '10.0.0.5',
    '192.168.1.1',
    '172.16.0.10',
    '10.0.0.5',
    '192.168.1.20'
];

const uniqueIPs = new Set(accessLogs);
console.log("唯一 IP 地址:", [...uniqueIPs]); // [ '192.168.1.1', '10.0.0.5', '172.16.0.10', '192.168.1.20' ]
console.log("唯一访问者数量:", uniqueIPs.size); // 4

6.3 实现标签系统中的标签管理

对于文章、产品等带有多个标签的系统,Set 可以很方便地管理和比较标签。

const articleTags = new Set(['JavaScript', 'Web Development', 'Frontend', 'Tutorial']);
const userInterests = new Set(['JavaScript', 'Backend', 'Data Science']);

// 找出文章和用户共同感兴趣的标签
const commonTags = intersection(articleTags, userInterests);
console.log("共同标签:", [...commonTags]); // [ 'JavaScript' ]

// 找出用户感兴趣但文章没有的标签
const missingTags = difference(userInterests, articleTags);
console.log("用户感兴趣但文章没有的标签:", [...missingTags]); // [ 'Backend', 'Data Science' ]

// 判断文章是否包含特定标签 (例如,是否是前端相关)
const frontendTags = new Set(['Frontend', 'CSS', 'HTML']);
console.log("文章是否是前端相关?", hasAnyRequiredRole(articleTags, frontendTags)); // true (因为有 Frontend)

6.4 检测图中的环(简化版)

在图论中,Set 可以用于跟踪深度优先搜索 (DFS) 或广度优先搜索 (BFS) 过程中已访问的节点,以检测环或避免重复访问。

const cyclicGraph = {
    A: ['B'],
    B: ['C'],
    C: ['A'] // 形成一个环 A -> B -> C -> A
};

const acyclicGraph = {
    A: ['B', 'C'],
    B: ['D'],
    C: ['E'],
    D: [],
    E: []
};

/**
 * 使用 DFS 检测图中是否存在环。
 * @param {Object} graph 图的邻接列表表示。
 * @param {string} startNode 起始节点。
 * @returns {boolean} 如果图中存在环,则返回 true。
 */
function detectCycleDFS(graph, startNode) {
    const visited = new Set();    // 记录已访问过的所有节点
    const recursionStack = new Set(); // 记录当前递归路径上的节点

    function dfs(node) {
        visited.add(node);
        recursionStack.add(node);

        for (const neighbor of graph[node] || []) {
            if (!visited.has(neighbor)) {
                if (dfs(neighbor)) {
                    return true;
                }
            } else if (recursionStack.has(neighbor)) {
                // 如果邻居节点在当前递归栈中,说明存在环
                return true;
            }
        }
        recursionStack.delete(node); // 退出当前节点的递归,将其从栈中移除
        return false;
    }

    // 考虑到图可能不连通,需要遍历所有节点作为起点
    for (const node in graph) {
        if (!visited.has(node)) {
            if (dfs(node)) {
                return true;
            }
        }
    }
    return false;
}

console.log("cyclicGraph 中是否存在环?", detectCycleDFS(cyclicGraph, 'A'));     // true
console.log("acyclicGraph 中是否存在环?", detectCycleDFS(acyclicGraph, 'A'));   // false

通过这些实际案例,我们可以看到 Set 对象如何以其独特的唯一性保证和高效操作,成为解决各类编程问题的利器。它不仅提供了一个处理唯一数据集合的优雅方式,更将数学集合论的强大抽象能力引入到我们的日常JavaScript编程中。

Set 对象是JavaScript语言中一个设计精良且功能强大的数据结构,它直接将数学集合的概念引入编程实践。通过其核心的唯一性保证和高效的 adddeletehas 操作,Set 成为处理去重、集合运算以及追踪唯一状态的理想选择。理解并熟练运用 Set,能够显著提升代码的清晰度、效率和可维护性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注