长久以来,数组一直是JavaScript中唯一的集合类型,不过,有一些开发者认为非数组对象也是集合,只不过是键值对集合,它们的用途与数组完全相同。在ES6之前,由于可选的集合类型有限,数组使用的又是数值型索引,因而经常被用于创建队列和栈。如果开发者们需要使用非数值型索引,就会用非数组对象创建所需的数据解构,而这是Set集合与Map集合的早期实现。
Set集合是一种无重复元素的列表,开发者们一般不会像访问数组元素那样逐一访问每个元素,通常的做法是检测给定的值在某个集合中是否存在。Map集合内含多组键值对,集合中每个元素分别存放着可访问的键名和它对应的值,Map集合经常被用于缓存频繁取用的数据。在ES6标准正式发布以前,开发者们已经在ES5中用非数组对象实现了类似的功能。
用对象模拟集合
var set = Object.create(null);
set.foo = true;
// 检查属性是否存在
if(set.foo){
// 要执行的代码
}
变量set是一个原型为null的对象,不继承任何属性,在ES5中,开发者们经常用类似的方法检测对象的某个属性是否存在,在这个示例中,将set.foo赋值为true,通过条件语句可以确认该值存在于当前对象中。
模拟Map集合唯一的区别在于存储的值不同。
var map = Object.create(null);
map.foo = "bar";
// 获取值
var value = map.foo;
// bar
console.log(value);
如果程序比较简单,确实可以用对象来模拟Set集合和Map集合,但如果触碰到对象属性的某些限制,那么这个方法就会变得更加复杂。比如所有对象的属性名必须是字符串类型 ,必须确保每个键名都是字符串类型且在对象中是唯一的。
var map = Object.create(null);
map[5] = "foo";
// foo
console.log(map["5"]);
// foo
console.log(map[5]);
本来属性名是数值型的 5,但是会自动转换为字符串,最后map[“5”]和map[5]引用的是同一个属性。因此如果想分别用数字和字符串作为对象属性的键名,则内部的自动转换机制会导致很多问题。
同样的,用对象最为属性的键名也会遇到类似的问题。
var map = Object.create(null);
var key1 = {},key2 = {};
map[key1] = "foo2";
// foo2
console.log(map[key2]);
// foo2
console.log(map["[object Object]"]);
对象也会自动转换为字符串作为键名。用不同对象作为对象属性的键名理论上应该指向多个属性,但实际上都是同一个属性"[object Object]"。由于对象会被转换为默认的字符串表达方式,因此其很难用作对象舒心的键名。
对于Map集合来说,如果他的属性值是假值,则在要求使用布尔值的情况下会被自动转为false。强制转换本身没有问题,但是某些场景下,就会导致错误发生。
var map = Object.create(null);
map.count = 0;
// 本来是检查count属性是否存在,实际上检查的是改值是否非零
if(map.count){
// 要执行的代码 这里并不会执行
console.log(map.count);
}
这个示例中,本来是检查map中是否包含count,但是count为0时,if语句中的代码却不会被执行。因为这里会被自动转换为false。
在大型软件中,一旦发生此类问题将难以定位及调试,从而ES6中需要加入Set集合和Map集合这两种新特性。
Set集合不会对值进行强制的类型转换,数字5和字符串"5"可以作为两个独立元素存在,引擎内部通过Object.is方法检测两个值是否一致,唯一的例外是,Set集合中的+0和-0被认为时相等的。
// 调用new Set创建Set集合
let set = new Set();
// 通过add方法添加元素
set.add(5);
set.add("5");
/*
* 通过size属性获取集合中目前的元素数量,Set集合不会对值进行强制的类型转换
* 数字5和字符串"5"可以作为两个独立元素存在
* 引擎内部通过Object.is方法检测两个值是否一致,唯一的例外是,Set集合中的+0和-0被认为时相等的
*/
// 2
console.log(set.size);
同样,向Set集合中添加两个对象,它们之间彼此保持独立。
let set = new Set(), key1 = {}, key2 = {};
set.add(key1);
set.add(key2);
// 2
console.log(set.size);
如果多次调用add方法并传入相同的值作为参数,那么后续的调用实际上会被忽略
let set = new Set();
set.add(5);
set.add("5");
// 重复 - 本次调用直接被忽略
set.add(5);
// 2
console.log(set.size);
由于第二次传入的数字5是一个重复值,因此不会被添加到集合中。
也可以用数组来初始化Set集合,Set构造函数同样会过滤掉重复的值从而保证集合中的元素各自唯一。
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
// 5
console.log(set.size);
自动去重的功能对于将已有代码或者JSON结构转换为Set集合执行得非常好。
实际上,Set构造函数可以接受所有可迭代对象作为参数,数组、Set集合、Map集合都是可迭代的,因而都可以作为Set构建函数的参数使用;构造函数通过迭代器从参数中提取值。
可以通过has()方法检测Set集合中是否存在某个值
let set = new Set();
set.add(5);
set.add("5");
// true
console.log(set.has(5));
// false
console.log(set.has(6));
通过delete()方法可以移出Set集合中的某一个元素,调用clear()方法会移除集合中的所有元素。
let set = new Set();
set.add(5);
set.add("5");
// true
console.log(set.has(5));
set.delete(5);
// false
console.log(set.has(5));
// 1
console.log(set.size);
set.clear();
// false
console.log(set.has("5"));
// 0
console.log(set.size);
forEach方法中的回调参数接受以下三个参数
let set = new Set([1,2]);
set.forEach(function(value,key,ownerSet){
console.log(key + " " + value);
console.log(ownerSet === set);
});
执行结果如下
1 1
true
2 2
true
在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集合转换为一个数组。
let set = new Set([1,2,3,3,3,4,5]),array = [...set];
// [1, 2, 3, 4, 5]
console.log(array);
删除数组中的重复元素,可以将数组转为集合然后再转为数组即可、
function eliminateDuplicates(items) {
return [...new Set(items)];
}
let nums = [1, 2, 3, 3, 3, 4, 5];
let noDuplicates = eliminateDuplicates(nums);
// [1, 2, 3, 4, 5]
console.log(noDuplicates);