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 === NaN为false,但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 中,key 和 value 是相同的)、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$。
实现思路: 可以通过两种方式实现:
- 计算 $(A setminus B) cup (B setminus A)$。
- 计算 $(A cup B) setminus (A cap B)$。
这里我们采用第一种方法,因为它直接使用了我们已经实现的 difference 和 union 函数。
/**
* 计算两个 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 在某些操作上比 Array 或 Object 具有显著的性能优势。
-
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语言中一个设计精良且功能强大的数据结构,它直接将数学集合的概念引入编程实践。通过其核心的唯一性保证和高效的 add、delete、has 操作,Set 成为处理去重、集合运算以及追踪唯一状态的理想选择。理解并熟练运用 Set,能够显著提升代码的清晰度、效率和可维护性。