30. 集合

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

1. 对象的键作为字符串集。
2. 数组作为任意值的集合(例如,通过.includes()检查元素是否在集合中),缺点是元素检查缓慢。

ECMAScript 6 具有数据结构Set,适用于任意值且具有快速执行元素检查。

30.1. 使用集合

30.1.1. 创建集合

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

  • 首先,您可以使用不带任何参数的构造函数来创建空集:
    const emptySet = new Set();
    assert.equal(emptySet.size, 0);
    
  • 其次,您可以将迭代(例如,数组)传递给构造函数。迭代值成为新集的元素:
    const set = new Set(['red', 'green', 'blue']);
    
  • 第三,.add()方法将元素添加到 Set 并且是可链式调用的:
    const set = new Set()
    .add('red')
    .add('green')
    .add('blue');
    

30.1.2. 添加,删除,检查包含

.add()Set 添加元素。 .has()检查某个元素是否包含在一个集合中。 .delete()Set 中删除元素。

const set = new Set();
set.add('red');
assert.equal(set.has('red'), true);
assert.equal(set.delete('red'), true); // there was a deletion
assert.equal(set.has('red'), false);

30.1.3. 确定 Set 的大小和清除

.size包含 Set 中的元素数量。 .clear()删除 Set 的所有元素。

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

30.1.4. 遍历集合

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

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

如您所见,集合保留插入顺序。也就是说,元素总是按照添加顺序迭代。集合是可迭代的,可以使用展开(...)方法将其转换为Array

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

30.2. 使用Set示例

30.2.1. 从数组中删除重复项

将数组转换为 Set 并返回,将从数组中删除重复项:

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

30.2.2 创建一个Unicode字符集合

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

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

30.3. 什么集合元素被认为是相等的?

Map的key一样,集合元素的比较类似于===,但NaN等于它自身。

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

第二次添加元素无效:

> const set = new Set();
> set.add('foo');
> set.size
1
> set.add('foo');
> set.size
1

===类似,两个不同的对象永远不会被认为是相同的(目前无法自定义):

> const set = new Set();
> set.add({});
> set.size
1
> set.add({});
> set.size
2

30.4. 缺少的 Set 操作

集合缺少几个常见操作。它们通常可通过以下方式实现:

  • 将集转换为数组通过展开(...)。
  • 对数组执行操作。
  • 将结果转换回 Set。

30.4.1. 并集(a∪b)

计算两个集合a和b的并集是指创建一个新的集合,包含a和b的所有元素。

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([...union], [1, 2, 3, 4]);

30.4.2. 交集(a∩b)

计算两个集合a和b的交集是指创建一个新的集合,包含同时包含在a和b中的元素。

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

30.4.3. 差集(a\b)

计算两个集合a和b的差集是指创建一个新的集合,包含那些在a中而不在b中的元素。该操作又称为减法(-)。

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

30.4.4. 集合映射

集合没有.map()方法。但是我们可以从Array中借用。

const set = new Set([1, 2, 3]);
const mappedSet = new Set([...set].map(x => x * 2));
// Convert mappedSet to an Array to check what’s inside it
assert.deepEqual([...mappedSet], [2, 4, 6]);

30.4.5. 集合过滤

我们不能直接使用.filter()操作集合,因此我们需要使用Array的相应方法。

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

30.5. 快速参考:Set<T>

30.5.1. 构造函数

  • new Set<T>(values?: Iterable<T>) ^[ES6]^ 如果未提供参数values,则会创建一个空集。如果提供values,则迭代值将作为元素添加到 Set 中。例如:
    const set = new Set(['red', 'green', 'blue']);
    

30.5.2. Set<T>.prototype:设置单个元素

  • .add(value: T): this ^[ES6]^ 将value添加到此 Set。此方法返回this,这意味着它可以链式调用。
    const set = new Set(['red']);
    set.add('green').add('blue');
    assert.deepEqual([...set], ['red', 'green', 'blue']);
    
  • .delete(value: T): boolean ^[ES6]^ 从此 Set 中删除value,如果删除成功返回true,反之返回false
    const set = new Set(['red', 'green', 'blue']);
    assert.equal(set.delete('red'), true); // there was a deletion
    assert.deepEqual([...set], ['green', 'blue']);
    
  • .has(value: T): boolean ^[ES6]^ 检查value是否在此集合中。
    const set = new Set(['red', 'green']);
    assert.equal(set.has('red'), true);
    assert.equal(set.has('blue'), false);
    

30.5.3. Set<T>.prototype:所有 Set 元素

  • get .size: number ^[ES6]^ 返回此 Set 中有多少元素。
    const set = new Set(['red', 'green', 'blue']);
    assert.equal(set.size, 3);
    
  • .clear(): void ^[ES6]^ 从此 Set 中删除所有元素。
    const set = new Set(['red', 'green', 'blue']);
    assert.equal(set.size, 3);
    set.clear();
    assert.equal(set.size, 0);
    

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

  • .values(): Iterable<T> ^[ES6]^ 返回包含该 Set 的所有元素的迭代器。
    const set = new Set(['red', 'green']);
    for (const x of set.values()) {
      console.log(x);
    }
    // Output:
    // 'red'
    // 'green'
    
  • [Symbol.iterator](): Iterable<T> ^[ES6]^ 迭代 Set 的默认方式。与.values()相同。
    const set = new Set(['red', 'green']);
    for (const x of set) {
      console.log(x);
    }
    // Output:
    // 'red'
    // 'green'
    
  • .forEach(callback: (value: T, value2: T, theSet: Set<T>) => void, thisArg?: any): void ^[ES6]^ 循环遍历此 Set 的元素,并为每个元素调用回调(第一个参数)。 valuekey都设置为当前元素,这种冗余设计是为了使该方法的回调与Map.prototype.forEach相同的类型签名。 可以通过thisArg指定this的回调。否则,this设置为undefined
    const set = new Set(['red', 'green']);
    set.forEach(x => console.log(x));
    // Output:
    // 'red'
    // 'green'
    

30.5.5. 与Map对称

仅存在以下两种方法,以使 Sets 的接口类似于 Maps 的接口。处理每个 Set 元素就好像它是一个 Map 键,其键和值是元素。

  • Set.prototype.entries(): Iterable<[T,T]> ^[ES6]^
  • Set.prototype.keys(): Iterable<T> ^[ES6]^

.entries()使您可以将 Set 转换为 Map:

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