写给 impatient 程序员的 JavaScript (ES2022 版)
请支持本书:购买捐赠
(广告,请不要屏蔽。)

35 集合 (Set)



在 ES6 之前,JavaScript 没有用于集合的数据结构。相反,使用了两种变通方法

从 ES6 开始,JavaScript 拥有数据结构 Set,它可以包含任意值并快速执行成员资格检查。

35.1 使用集合

35.1.1 创建集合

创建集合有三种常用方法。

首先,您可以使用不带任何参数的构造函数来创建一个空集合

const emptySet = new Set();
assert.equal(emptySet.size, 0);

其次,您可以将可迭代对象(例如,数组)传递给构造函数。迭代值将成为新集合的元素

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

第三,.add() 方法将元素添加到集合中,并且可以链接

const set = new Set()
.add('red')
.add('green')
.add('blue');

35.1.2 添加、删除、检查成员

.add() 将元素添加到集合中。

const set = new Set();
set.add('red');

.has() 检查元素是否是集合的成员。

assert.equal(set.has('red'), true);

.delete() 从集合中删除元素。

assert.equal(set.delete('red'), true); // there was a deletion
assert.equal(set.has('red'), false);

35.1.3 确定集合的大小并清空它

.size 包含集合中元素的数量。

const set = new Set()
  .add('foo')
  .add('bar');
assert.equal(set.size, 2)

.clear() 删除集合中的所有元素。

set.clear();
assert.equal(set.size, 0)

35.1.4 迭代集合

集合是可迭代的,for-of 循环按预期工作

const set = new Set(['red', 'green', 'blue']);
for (const x of set) {
  console.log(x);
}
// Output:
// 'red'
// 'green'
// 'blue'

如您所见,集合保留*插入顺序*。也就是说,始终按照添加元素的顺序迭代元素。

鉴于集合是可迭代的,您可以使用 Array.from() 将它们转换为数组

const set = new Set(['red', 'green', 'blue']);
const arr = Array.from(set); // ['red', 'green', 'blue']

35.2 使用集合的示例

35.2.1 从数组中删除重复项

将数组转换为集合并转换回来,可以删除数组中的重复项

assert.deepEqual(
  Array.from(new Set([1, 2, 1, 2, 3, 3, 3])),
  [1, 2, 3]);

35.2.2 创建 Unicode 字符集(代码点)

字符串是可迭代的,因此可以用作 new Set() 的参数

assert.deepEqual(
  new Set('abc'),
  new Set(['a', 'b', 'c']));

35.3 哪些集合元素被认为是相等的?

与 Map 键一样,集合元素的比较方式类似于 ===,但 NaN 等于自身除外。

> const set = new Set([NaN, NaN, NaN]);
> set.size
1
> set.has(NaN)
true

=== 一样,两个不同的对象永远不会被认为是相等的(目前无法改变这一点)

> const set = new Set();

> set.add({});
> set.size
1

> set.add({});
> set.size
2

35.4 缺少的集合操作

集合缺少几种常见操作。此类操作通常可以通过以下方式实现

35.4.1 并集 (ab)

计算两个集合 ab 的并集意味着创建一个包含 ab 中所有元素的集合。

const a = new Set([1,2,3]);
const b = new Set([4,3,2]);
// Use spreading to concatenate two iterables
const union = new Set([...a, ...b]);

assert.deepEqual(Array.from(union), [1, 2, 3, 4]);

35.4.2 交集 (ab)

计算两个集合 ab 的交集意味着创建一个包含 a 中也存在于 b 中的元素的集合。

const a = new Set([1,2,3]);
const b = new Set([4,3,2]);
const intersection = new Set(
  Array.from(a).filter(x => b.has(x))
);

assert.deepEqual(
  Array.from(intersection), [2, 3]
);

35.4.3 差集 (a \ b)

计算两个集合 ab 的差集意味着创建一个包含 a 中不存在于 b 中的元素的集合。此操作有时也称为*减* (−)。

const a = new Set([1,2,3]);
const b = new Set([4,3,2]);
const difference = new Set(
  Array.from(a).filter(x => !b.has(x))
);

assert.deepEqual(
  Array.from(difference), [1]
);

35.4.4 映射集合

集合没有 .map() 方法。但我们可以借用数组的方法

const set = new Set([1, 2, 3]);
const mappedSet = new Set(
  Array.from(set).map(x => x * 2)
);

// Convert mappedSet to an Array to check what’s inside it
assert.deepEqual(
  Array.from(mappedSet), [2, 4, 6]
);

35.4.5 过滤集合

我们不能直接对集合使用 .filter(),因此我们需要使用相应的数组方法

const set = new Set([1, 2, 3, 4, 5]);
const filteredSet = new Set(
  Array.from(set).filter(x => (x % 2) === 0)
);

assert.deepEqual(
  Array.from(filteredSet), [2, 4]
);

35.5 快速参考:Set<T>

35.5.1 构造函数

35.5.2 Set<T>.prototype:单个集合元素

35.5.3 Set<T>.prototype:所有集合元素

35.5.4 Set<T>.prototype:迭代和循环

35.5.5 与 Map 的对称性

以下两种方法主要存在是为了使集合和映射具有相似的接口。每个集合元素都被视为其键和值都是该元素的 Map 条目。

.entries() 使您能够将集合转换为映射

const set = new Set(['a', 'b', 'c']);
const map = new Map(set.entries());
assert.deepEqual(
  Array.from(map.entries()),
  [['a','a'], ['b','b'], ['c','c']]
);

35.6 常见问题解答:集合

35.6.1 为什么集合有 .size,而数组有 .length

这个问题的答案在 §33.6.4 “为什么映射有 .size,而数组有 .length?” 中给出。

  测验

参见 测验应用程序