ES6 的 Set
ES6
新增了 Set
类型,这是一种无重复值的有序列表。Set
允许对它包含的数据进行快速访问,从而增加了一个追踪离散值的更有效方式。
创建 Set 并添加项目
Set
使用 new Set()
来创建,而调用 add()
方法就能向 Set
中添加项目,检查 size
属性还能查看其中包含有多少项:
let set = new Set();
set.add(5);
set.add("5");
console.log(set.size); // 2
Set
不会使用强制类型转换来判断值是否重复。这意味着 Set
可以同时包含数值 5
与字符串 "5"
,将它们都作为相对独立的项(在 Set
内部的比较使用了第四章讨论过的 Object.is()
方法,来判断两个值是否相等,唯一的例外是 +0
与 -0
在 Set
中被判断为是相等的)。你还可以向 Set
添加多个对象,它们不会被合并为同一项:
let set = new Set(),
key1 = {},
key2 = {};
set.add(key1);
set.add(key2);
console.log(set.size); // 2
由于 key1
与 key2
并不会被转换为字符串,所以它们在这个 Set
内部被认为是两个不同的项(记住:如果它们被转换为字符串,那么都会等于 "[object Object]"
)。
如果 add()
方法用相同值进行了多次调用,那么在第一次之后的调用实际上会被忽略:
let set = new Set();
set.add(5);
set.add("5");
set.add(5); // duplicate - this is ignored
console.log(set.size); // 2
你可以使用数组来初始化一个 Set
,并且 Set
构造器会确保不重复地使用这些值。例如:
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
console.log(set.size); // 5
在此例中,带有重复值的数组被用来初始化这个 Set
。虽然数值 5
在数组中出现了四次,但 Set
中却只有一个 5
。若要把已存在的代码或 JSON
结构转换为 Set
来使用,这种特性会让转换更轻松。
|
你可以使用 has()
方法来测试某个值是否存在于 Set
中,就像这样:
let set = new Set();
set.add(5);
set.add("5");
console.log(set.has(5)); // true
console.log(set.has(6)); // false
此处的 Set
不包含 6
这个值,因此 set.has(6)
会返回 false
。
移除值
也可以从 Set
中将值移除。你可以使用 delete()
方法来移除单个值,或调用 clear()
方法来将所有值从 Set
中移除。以下代码展示了二者的作用:
let set = new Set();
set.add(5);
set.add("5");
console.log(set.has(5)); // true
set.delete(5);
console.log(set.has(5)); // false
console.log(set.size); // 1
set.clear();
console.log(set.has("5")); // false
console.log(set.size); // 0
在调用 delete()
之后,只有 5
被移走;而执行 clear()
方法后,set
就被清空了。所有这些方法都提供了一个非常简单的机制来追踪有序的唯一值。不过,在给 Set
添加项之后,要如何对每个项执行一些操作呢?此时 forEcah()
方法就派上用场了。
Set 上的 forEach() 方法
若你曾处理过数组,可能就已经熟悉了 forEach()
方法。ES5
给数组添加了 forEach()
方法,使得更易处理数组中的每一项,而无须建立 for
循环。该方法被开发者普遍使用,于是 Set
类型也添加了相同方法,其工作方式也一样。
forEach()
方法会被传递一个回调函数,该回调接受三个参数:
-
Set
中下个位置的值; -
与第一个参数相同的值;
-
目标
Set
自身。
Set
版本的 forEach()
方法与数组版本有个奇怪差异:前者传给回调函数的第一个与第二个参数是相同的。虽然看起来像是错误,但这种行为却有个正当理由。
具有 forEach()
方法的其他对象(即数组与 Map)都会给回调函数传递三个参数,前两个参数都分别是下个位置的值与键(给数组使用的键是数值索引)。
然而 Set
却没有键。ES6
标准的制定者本可以将 Set
版本的 forEach()
方法的回调函数设定为只接受两个参数,但这会让它不同于另外两个版本的方法。不过他们找到了一种方式让这些回调函数保持参数相同:将 Set
中的每一项同时认定为键与值。于是为了让 Set
的 forEach()
方法与数组及 Map
版本的保持一致,该回调函数的前两个参数就始终相同了。
除了参数特点的差异外,在 Set
上使用 forEach()
方法与在数组上基本相同。这里有些代码展示了该方法如何工作:
let set = new Set([1, 2]);
set.forEach(function(value, key, ownerSet) {
console.log(key + " " + value);
console.log(ownerSet === set);
});
此代码在 Set
的每一项上进行迭代,并对传递给 forEach()
的回调函数的值进行了输出。回调函数每次执行时,key
与 value
总是相同的,同时 ownerSet
也始终等于 set
。此代码输出:
1 1
true
2 2
true
与使用数组相同,如果想在回调函数中使用 this
,你可以给 forEach()
传入一个 this
值作为第二个参数:
let set = new Set([1, 2]);
let processor = {
output(value) {
console.log(value);
},
process(dataSet) {
dataSet.forEach(function(value) {
this.output(value);
}, this);
}
};
processor.process(set);
本例中 processor.process()
方法在 Set
上调用了 forEach()
,并传递了当前 this
作为回调函数的 this
值。这个传递十分必要,这样 this.output()
就能正确地解析到 processor.output()
方法。此处 forEach()
的回调函数仅使用了第一个参数 value
,其余参数则被省略了。你也可以使用箭头函数来达到相同效果,而无须传入第二个参数,就像这样:
let set = new Set([1, 2]);
let processor = {
output(value) {
console.log(value);
},
process(dataSet) {
dataSet.forEach((value) => this.output(value));
}
};
processor.process(set);
本例中的箭头函数读取了包含它的 process()
函数的 this
值,因此就能正确地将 this.output()
解析为调用 processor.output()
。
要记住,虽然 Set
能非常好地追踪值,并且 forEach()
可以让你按顺序处理每一项,但是却无法像数组那样用索引来直接访问某个值。如果你想这么做,最好的选择是将 Set
转换为数组。
将 Set 转换为数组
将数组转换为 Set
相当容易,因为可以将数组传递给 Set
构造器;而使用扩展运算符也能简单地将 Set
转换回数组。第三章介绍的扩展运算符(…
),能将数组中的项分割开并作为函数的分离参数。你同样能将扩展运算符用于可迭代对象(例如 Set
),将它们转换为数组。例如:
let set = new Set([1, 2, 3, 3, 3, 4, 5]),
array = [...set];
console.log(array); // [1,2,3,4,5]
此处的 Set
在初始化时载入了一个包含重复值的数组。Set
清除了重复值之后,又使用了扩展运算符将自身的项放到一个新数组中。而这个 Set
仍然包含在创建时所接收的项(1、2、3、4 与 5),这些项只是被复制到了新数组中,而并未从 Set
中消失。
当已经存在一个数组,而你想用它创建一个无重复值的新数组时,该方法十分有用。例如:
function eliminateDuplicates(items) {
return [...new Set(items)];
}
let numbers = [1, 2, 3, 3, 3, 4, 5],
noDuplicates = eliminateDuplicates(numbers);
console.log(noDuplicates); // [1,2,3,4,5]
在 eliminateDuplicates()
函数中,Set
只是一个临时的中介物,以便在创建一个无重复的数组之前将重复值过滤掉。
Weak Set
由于 Set
类型存储对象引用的方式,它也可以被称为 Strong Set
。对象存储在 Set
的一个实例中时,实际上相当于把对象存储在变量中。只要对于 Set
实例的引用仍然存在,所存储的对象就无法被垃圾回收机制回收,从而无法释放内存。例如:
let set = new Set(),
key = {};
set.add(key);
console.log(set.size); // 1
// eliminate original reference
key = null;
console.log(set.size); // 1
// get the original reference back
key = [...set][0];
在本例中,将 key
设置为 null
清除了对 key
对象的一个引用,但是另一个引用还存于 set
内部。你仍然可以使用扩展运算符将 Set
转换为数组,然后访问数组的第一项,key
变量就取回了原先的对象。这种结果在大部分程序中是没问题的,但有时,当其他引用消失之后若 Set
内部的引用也能消失,那就更好。例如,当 JS
代码在网页中运行,同时你想保持与 DOM
元素的联系,在该元素可能被其他脚本移除的情况下,你应当不希望自己的代码保留对该 DOM
元素的最后一个引用(这种情况被称为内存泄漏)。
为了缓解这个问题,ES6
也包含了 Weak Set
,该类型只允许存储对象弱引用,而不能存储基本类型的值。对象的弱引用在它自己成为该对象的唯一引用时,不会阻止垃圾回收。
创建 Weak Set
Weak Set
使用 WeakSet
构造器来创建,并包含 add()
方法、has()
方法以及 delete()
方法。以下例子使用了这三个方法:
let set = new WeakSet(),
key = {};
// add the object to the set
set.add(key);
console.log(set.has(key)); // true
set.delete(key);
console.log(set.has(key)); // false
使用 Weak Set
很像在使用正规的 Set
。你可以在 Weak Set
上添加、移除或检查引用,也可以给构造器传入一个可迭代对象来初始化 Weak Set
的值:
let key1 = {},
key2 = {},
set = new WeakSet([key1, key2]);
console.log(set.has(key1)); // true
console.log(set.has(key2)); // true
在本例中,一个数组被传给了 WeakSet
构造器。由于该数组包含了两个对象,这些对象就被添加到了 Weak Set
中。要记住若数组中包含了非对象的值,就会抛出错误,因为 WeakSet
构造器不接受基本类型的值。
Set 类型之间的关键差异
Weak Set
与正规 Set
之间最大的区别是对象的弱引用。此处有个例子说明了这种差异:
let set = new WeakSet(),
key = {};
// add the object to the set
set.add(key);
console.log(set.has(key)); // true
// remove the last strong reference to key, also removes from weak set
key = null;
当此代码被执行后,Weak Set
中的 key
引用就不能再访问了。核实这一点是不可能的,因为需要把对于该对象的一个引用传递给 has()
方法(而只要存在其他引用,Weak Set
内部的弱引用就不会消失)。这会使得难以对 Weak Set
的引用特征进行测试,但 JS
引擎已经正确地将引用移除了,这一点你可以信任。
这些例子演示了 Weak Set
与正规 Set
的一些共有特征,但是它们还有一些关键的差异,即:
-
对于
WeakSet
的实例,若调用add()
方法时传入了非对象的参数,就会抛出错误(has()
或delete()
则会在传入了非对象的参数时返回false
); -
Weak Set
不可迭代,因此不能被用在for-of
循环中; -
Weak Set
无法暴露出任何迭代器(例如keys()
与values()
方法),因此没有任何编程手段可用于判断Weak Set
的内容; -
Weak Set
没有forEach()
方法; -
Weak Set
没有size
属性。
Weak Set
看起来功能有限,而这对于正确管理内存而言是必要的。一般来说,若只想追踪对象的引用,应当使用 Weak Set
而不是正规 Set
。
Set
给了你处理值列表的新方式,不过若需要给这些值添加额外信息,它就没用了。这就是 ES6
还添加了 Map
类型的原因。