ES6中Set集合(与java里类似)
一、引入背景
Set集合是一种无重复元素的列表,开发者们一般不会逐一读取数组中的元素,也不太可能逐一访问Set集合中的每个元素,通常的做法是检测给定的值在某个集合中是否存在
Map集合内含多组键值对,集合中每个元素分别存放着可访问的键名和它对应的值,Map集合经常被用于缓存频繁取用的数据。在标准正式发布以前,开发者们已经在ES5中用非数组对象实现了类似的功能
ES6新标准将Set集合与Map集合添加到JS中。
在ES5中,开发者们用对象属性来模拟这两种集合
let set = Object.create(null);
set.foo = true;
// 检查属性的存在性
if (set.foo) {
// 一些操作
}
这里的变量set是一个原型为null的对象,不继承任何属性。在ES5中,开发者们经常用类似的方法检查对象的某个属性值是否存在。 在这个示例中,将set.foo赋值为true,通过if语句可以确认该值存在于当前对象中
模拟这两种集合对象的唯一区别是存储的值不同,以下这个示例是用对象模拟Map集合
let map = Object.create(null);
map.foo = "bar";
// 提取一个值
let value = map.foo;
console.log(value); // "bar"
这段代码将字符串"bar"储存在map.foo中。一般来说,Set集合常被用于检查对象中是否存在某个键名,而Map集合常被用于获取已存的信息
如果程序很简单,确实可以用对象来模拟Set集合与Map集合,但如果触碰到对象属性的某些限制,那么这个方法就会变得更加复杂。例如,所有对象的属性名必须是字符串类型,必须确保每个键名都是字符串类型且在对象中是唯一的
let map = Object.create(null);
map[5] = "foo";
console.log(map["5"]); // "foo"
本例中将对象的某个属性赋值为字符串"foo",而这个属性的键名是数值型的5,它会被自动转换成字符串,所以map["5"]和map[5]引用的其实是同一个属性。如果想分别用数字和字符串作为对象属性的键名,则内部的自动转换机制会导致很多问题。当然,用对象作为属性的键名也会遇到类似的问题
let map = Object.create(null),
key1 = {},
key2 = {};
map[key1] = "foo";
console.log(map[key2]); // "foo"
由于对象属性的键名必须是字符串,因而这段代码中的key1和key2将被转换为对象对应的默认字符串"[object Object]",所以map[key2]和map[key1]引用的是同一个属性。这种错误很难被发现,用不同对象作为对象属性的键名理论上应该指向多个属性,但实际上这种假设却不成立
由于对象会被转换为默认的字符串表达方式,因此其很难用作对象属性的键名
对于Map集合来说,如果它的属性值是假值,则在要求使用布尔值的情况下(例如在if语句中)会被自动转换成false。强制转换本身没有问题,但如果考虑这个值的使用场景,就有可能导致错误发生
let map = Object.create(null);
map.count = 1;
// 是想检查 "count" 属性的存在性,还是想检查非零值?
if (map.count) {
// ...
}
这个示例中有一些模棱两可的地方,比如我们应该怎样使用map.count?if语句中,我们是检查map.count是否存在,还是检查值是否非零。在示例中,由于value的值是1,为真值,if语句中的代码将被执行。然而,如果map.count的值为0或者不存在,if语句中的代码块将不会被执行
在大型软件应用中,一旦发生此类问题将难以定位及调试,从而促使ES6在语言中加入Set集合与Map集合这两种新特性
当然,在JS中有一个in运算符,不需要读取对象的值就可以判断属性在对象中是否存在,如果存在就返回true。但是,in运算符也会检索对象的原型,只有当对象原型为null时使用这个方法才比较稳妥
二、Set集合
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。通过Set集合可以快速访问其中的数据,更有效地追踪各种离散值
Set 结构的实例有以下属性:(类似java里面)
Set.prototype.constructor:构造函数,默认就是Set函数
Set.prototype.size:返回Set实例的成员总数
Set 实例的操作方法(用于操作数据)包括以下4个:
add(value):添加某个值,返回Set结构本身
has(value):返回一个布尔值,表示该值是否为Set的成员
delete(value):删除某个值,返回一个布尔值,表示删除是否成功
clear():清除所有成员,没有返回值
1、创建Set集合、add()添加元素
调用new Set()创建Set集合,调用add()方法向集合中添加元素,访问集合的size属性可以获取集合中目前的元素数量
let set = new Set();
set.add(5);
set.add("5");
console.log(set.size); // 2
在Set集合中,不会对所存值进行强制的类型转换,数字5和字符串"5"可以作为两个独立元素存在
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
上面代码通过add
方法向 Set 结构加入成员,结果表明 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); // 重复了,该调用被忽略
console.log(set.size); // 2
由于第二次传入的数字5是一个重复值,因此其不会被添加到集合中,所以控制台最后输出的Set集合size属性值为2
可以使用数组来初始化一个 Set ,并且 Set 构造器会确保不重复地使用这些值
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
console.log(set.size); // 5
在这个示例中,我们用一个含重复元素的数组来初始化Set集合,数组中有4个数字5,而在生成的集合中只有一个。自动去重的功能对于将已有代码或JSON结构转换为Set集合执行得非常好
实际上,Set构造函数可以接受所有可迭代对象作为参数,数组、Set集合、Map集合都是可迭代的,因而都可以作为Set构造函数的参数使用;构造函数通过迭代器从参数中提取值
2、has()检测元素
通过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
3、delete()和clear()移除元素
调用delete()方法可以移除Set集合中的某一个元素,调用clear()方法会移除集合中的所有元素
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)之后,只有数字5被移除;执行clear()方法后,Set集合中的所有元素都被清除了
4、遍历操作
Set 结构的实例有四个遍历方法,可以用于遍历成员
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员
5、keys()、values()、entries()
keys
方法、values
方法、entries
方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys
方法和values
方法的行为完全一致(map的话就不一样)
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
上面代码中,entries
方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等(对于set集合来说,entries返回的键名和键值完全相等)
Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values
方法
Set.prototype[Symbol.iterator] === Set.prototype.values// true
let set = new Set(['red', 'green', 'blue']);
for (let x of set) {
console.log(x);
}
// red
// green
// blue
这意味着,可以省略values方法,直接用for...of循环遍历 Set
6、forEach()
Set结构的实例的forEach
方法,用于对每个成员执行某种操作,没有返回值
let set = new Set(['a','b','c']);
set.forEach((key, value, set) => { console.log(key,value,set);} )
//a a ['a','b','c']
//b b ['a','b','c']
//c c ['a','b','c']
上面代码说明,forEach
方法的参数就是一个处理函数。该函数的参数依次为键值、键名、集合本身
在Set集合的forEach()方法中,第二个参数也与数组的一样,如果需要在回调函数中使用this引用,则可以将它作为第二个参数传入forEach()函数
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,所以直接省略了其他参数。在这里也可以使用箭头函数,这样就无须再将this作为第二个参数传入回调函数了
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集合转换成一个数组
7、将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集合依然保留创建时接受的元素(1、2、3、4、5),新数组中保存着这些元素的副本
如果已经创建过一个数组,想要复制它并创建一个无重复元素的新数组,则上述这个方法就非常有用
function eliminateDuplicates(items) {
return [...new Set(items)];//先用set构造方法创建set,再用展开运算符将set转为无重复元素的数组
}
let numbers = [1, 2, 3, 3, 3, 4, 5],
noDuplicates = eliminateDuplicates(numbers);
console.log(noDuplicates); // [1,2,3,4,5]
在以上函数中,Set集合仅是用来过滤重复值的临时中介,最后会输出新创建的无重复元素的数组
三、WeakSet
将对象存储在Set的实例与存储在变量中完全一样,只要Set实例中的引用存在,垃圾回收机制就不能释放该对象的内存空间,于是之前提到的Set类型可以被看作是一个强引用的Set集合
let set = new Set(),
key = {};
set.add(key);
console.log(set.size); // 1
// 取消原始引用
key = null;
console.log(set.size); // 1
// 重新获得原始引用
key = [...set][0];
在这个示例中,将变量key设置为null时便清除了对初始对象的引用,但是Set集合却保留了这个引用,仍然可以使用展开运算符将Set集合转换成数组格式并从数组的首个元素取出该引用
大部分情况下这段代码运行良好,但有时候会希望当其他所有引用都不再存在时,让Set集合中的这些引用随之消失。举个例子,如果在Web页面中通过JS代码记录了一些DOM元素,这些元素有可能被另一段脚本移除,而又不希望自己的代码保留这些DOM元素的最后一个引用
为了解决这个问题,ES6中引入了另外一个类型:WeakSet集合(弱引用Set集合)
1、创建WeakSet集合
用Weakset构造函数可以创建WeakSet集合,集合支持3个方法:add()、has()和delete()
let set = new WeakSet(),
key = {};
// 将对象加入 set
set.add(key);
console.log(set.has(key)); // true
set.delete(key);
console.log(set.has(key)); // false
WeakSet集合的使用方式与Set集合类似,可以向集合中添加引用,从中移除引用,也可以检査集合中是否存在指定对象的引用。也可以调用WeakSet构造函数并传入一个可迭代对象来创建WeakSet集合
let key1 = {},
key2 = {},
set = new WeakSet([key1, key2]);
console.log(set.has(key1)); // true
console.log(set.has(key2)); // true
以上示例中,向WeakSet构造函数传入一个含有两个对象的数组,最终创建包含这两个对象的WeakSet集合
2、与Set集合的区别
WeakSet与Set最大的区别是WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中
let set = new WeakSet(),
key = {};
set.add(key);
console.log(set.has(key)); // true
// 取消原始引用
key = null;
console.log(set.has(key)); // false
由于上面这个特点,WeakSet的成员是不适合引用的,因为它会随时消失。另外,由于WeakSet内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此ES6规定WeakSet不可遍历
除了以上主要区别之外,它们之间还有下面几个差别
(1)在Weakset的实例中,如果向add()、has()和delete()这3个方法传入非对象参数都会导致程序报错
(2)WeakSet集合不可迭代,所以不能被用于for-of循环
(3)WeakSet集合不暴露任何迭代器(例如keys()和values()方法),所以无法通过程序本身来检测其中的内容
(4)WeakSet集合不支持forEach()方法
(5)WeakSet集合不支持size属性
WeakSet集合的功能看似受限,其实这是为了让它能够正确地处理内存中的数据。总之,如果只需要跟踪对象引用,更应该使用Weak Set集合而不是普通的Set集合