Set 是不包含重复值的列表。你一般不会像对待数组那样来访问 Set 中的某个项。相反更常见的是,只在 Set 中检查某个值是否存在。
Map 则是键与相对应的值的集合。因此,Map 中的每个项都存储了两块数据,通过指定所需读取的键即可检索对应的值。Map 常被用作缓存,存储数据以便此后快速检索。
ES5 中的 Set 与 Map
在 ES5 中,开发者使用对象属性来模拟 Set 与 Map,就像这样:
1  | let set = Object.create(null)  | 
本例中的 set 变量是一个原型为 null 的对象,确保在此对象上没有继承属性。使用对象的属性作为需要检查的唯一值在 ES5 中是很常用的方法。当一个属性被添加到 set 对象时,它的值也被设为 true,因此条件判断语句就可以简单判断出该值是否存在。
使用对象模拟 Set 与模拟 Map 之间唯一真正的区别是所存储的值。例如:以下例子将对象作为 Map 使用:
1  | let map = Object.create(null)  | 
此代码将字符串值 "bar" 存储在 foo 键上。与 Set 不同,Map 多数被用来提取数据,而不是仅检查键的存在性。
变通方法的问题
尽管在简单情况下将对象作为 Set 与 Map 来使用都是可行的,但一旦接触到对象属性的局限性,此方式就会遇到更多麻烦。研究如下代码:
1  | let map = Object.create(null)  | 
造成上面例子中问题的原因是对象属性只可以是字符串或符号类型,所以当你为对象定义属性时,如果属性名不是字符串或符号类型,那么它会调用属性名的 toString() 方法将属性名转换为字符串,然后再进行后续操作。所以 map[key1] === map[key2]、map[key3] === map[key4]。
ES6 的 Set
ES6 新增了 Set 类型,这是一种无重复值的有序列表。Set 允许对它包含的数据进行快速访问,从而增加了一个追踪离散值的更有效方式。
创建 Set 并添加项目
Set 使用 new Set() 来创建,而调用 add() 方法就能向 Set 中添加项目,检查 size 属性还能查看其中包含有多少项。
1  | let set = new Set()  | 
Set 不会使用强制类型转换来判断值是否重复。这意味着 Set 可以同时包含数值 5 与字符串 "5",将它们都作为相对独立的项(在 Set 内部的比较使用了 Object.is() 方法来判断两个值是否相等,唯一的例外是+0与-0被判断为是相等的)。你还可以向 Set 添加多个对象,它们不会被合并为同一项:
1  | let set = new Set()  | 
由于 key1 与 key2 并不会被转换为字符串,所以它们在这个 Set 内部被认为是两个不同的项(记住:如果它们被转换为字符串,那么都会等于 "[object Object]")。
如果 add() 方法用相同值进行了多次调用,那么在第一次之后的调用实际上会被忽略:
1  | let set = new Set()  | 
你可以使用数组来初始化一个 Set,并且 Set 构造器会确保不重复地使用这些值。
1  | let set = new Set([1, 2, 3, 4, 5, 5, 5, 5])  | 
Set 构造器实际上可以接收任意可迭代对象作为参数。能使用数组是因为它们默认就是可迭代的,Map 也是一样的。
你可以使用 has() 方法来检测某个值是否存在于 Set 中,就像这样:
1  | let set = new Set()  | 
移除值
使用 delete() 方法可以移除 Set 中的某个值,使用 clear() 方法可以移除 Set 中的所有值。
1  | let set = new Set()  | 
Set 上的 forEach() 方法
Set上 的 forEach() 方法类似于数组中的 forEach() 方法,它接收两个参数:要在每一项上运行的函数和(可选的)运行该函数时的 this 值。传入的函数会接收三个参数:Set 元素的值,Set 元素的值和目标 Set 自身。你没看错,第一个和第二个参数是完全相同的,这么设计的目的是为了保持所有对象上 forEach() 方法的统一。
将 Set 转换为数组
使用扩展运算符可以很轻松的将 Set 转换为数组,用一个数组去重的例子来展示它是如何转换的:
1  | function eliminateDuplicates (array) {  | 
Weak Set
由于 Set 类型存储对象引用的方式,它也可以被称为 Strong Set。对象存储在 Set 的一个实例中时,实际上相当于把对象存储在变量中。只要对 Set 实例的引用仍然存在,所存储的对象就无法被垃圾回收机制回收,从而无法释放内存。例如:
1  | let set = new Set()  | 
在本例中,将 key 设置为 null 清楚了对 key 对象的一个引用,但是另一个引用还存在于 set 内部。你仍然可以使用扩展运算符将 Set 转换为数组,然后访问数组的第一项,key 变量就取回了原先的对象。
为了缓解这个问题,ES6 也包含了 Weak Set,该类型只允许存储对象弱引用,而不能存储基本类型的值。对象的弱引用在它自己成为该对象的唯一引用时,不会阻止垃圾回收。
创建 Weak Set
Weak Set 使用 WeakSet 构造器来创建,并包含 add() 方法、has() 方法以及 delete() 方法。以下例子使用了这三个方法:
1  | let set = new WeakSet()  | 
使用 Weak Set 很像在使用正规的 Set。你可以在 Weak Set 上添加、移除或检查引用,也可以给构造器传入一个可迭代对象来初始化 Weak Set的值:
1  | let key1 = {}  | 
在本例中,一个数组被传给了 WeakSet 构造器。由于该数组包含了两个对象,这些对象就被添加到了 Weak Set 中。要记住若数组中包含了非对象的值,就会抛出错误,因为 WeakSet 构造器不接受基本类型的值。
Set 类型之间的关键差异
Weak Set 与正规 Set 之间最大的区别是对象的弱引用。此处有个例子说明了这种差异:
1  | let set = new WeakSet()  | 
当代码被执行后,Weak Set 中的 key 引用就不能再访问了。核实这一点是不可能的,因为需要把对于该对象的一个引用传递给 has() 方法(而只要存在其他引用,Weak Set 内部的弱引用就不会消失)。这会使得难以对 Weak Set 的引用特征进行测试,但 JS 引擎已经正确地将引用移除了,这一点你可以信任。
这些例子演示了 Weak Set 与正规 Set 的一些共有特征,但是它们还有一些关键的差异,即:
- 对于 
WeakSet的实例,若调用add()方法时传入非对象的参数,就会抛出错误(has()或delete()则会在传入了非对象的参数时返回false)。 - Weak Set 不可迭代,因此不能被用在 
for-of循环中。 - Weak Set 无法暴露出任何迭代器,因此没有任何编程手段可用于判断 Weak Set 的内容。
 - Weak Set 没有 
forEach()方法。 - Weak Set 没有 
clear()方法。 - Weak Set 没有 
size属性。 
ES6 的 Map
ES6 的 Map 类型是键值对的有序列表,而键和值都可以是任意类型。
创建 Map
Map 对键的处理方式与 Set 对值的处理一样,采用的是 Object.is() 方法,而不会进行强制类型转换,唯一的例外是 +0 与 -0 被判断为是相等的。
你可以调用 set() 方法并给它传递一个键与一个关联的值,来给 Map 添加项。此后使用键名来调用 get() 方法便能提取对应的值。如果任意一个键不存在于Map中,则 get() 方法就会返回特殊值 undefined。例如:
1  | let map = new Map()  | 
此代码使用了对象 key1 与 key2 作为 Map 的键,并存储了两个不同的值。由于这些键不会被强制转换成其他形式,每个对象就都被认为是唯一的。这允许你给对象关联额外数据,而无须修改对象自身。
与 Set 类似,你能将数组传递给 Map 构造器,以便使用数据来初始化一个 Map。该数组中的每一项也必须是数组,内部数组的首个项会作为键,第二项则为对应值。因此整个 Map 就被这些双项数组所填充。例如:
1  | let map = new Map([['name', 'Nicholas'], ['age', 25]])  | 
虽然有数组构成的数组看起来有点奇怪,但这对于准确表示键来说却是必要的。因为键允许是任意数据类型,将键存储在数组中,是确保它们在被添加到 Map 之前不会被强制转换为其他类型的唯一方法。
Map 的方法及属性
has(key):判断指定的键是否存在于 Map 中。delete(key):移除 Map 中的键以及对应的值。clear():移除 Map 中所有的键与值。size:用于指示包含了多少个键值对。
Map 上的 forEach 方法
Map 的 forEach() 方法类似于 Set 与数组上的 forEach() 方法,不过它是按照键值对被添加到 Map 中的顺序来迭代的。它接收两个参数:要在每一项上运行的函数和(可选的)运行该函数时的 this 值。传入的函数会接收三个参数:Map 项的值、该值所对应的的键和目标Map自身。
Weak Map
使用Weak Map
ES6 的 WeakMap 类型是键值对的无序列表,其中键必须是非空的对象,值则允许是任意类型。WeakMap 的接口与 Map 的非常相似,都使用 set() 与 get() 方法来分别添加与提取数据。
类似于 Weak Set,它没有 size 属性,没有任何办法可以确定 Weak Map 是否为空。在其他引用被移除后,由于对键的引用不再有残留,也就无法调用 get() 方法来去取对应的值。Weak Map 已经切断了对于该值的访问,其所占的内存在垃圾回收器运行时便会被释放。
Weak Map 的初始化
为了初始化 Weak Map,需要把一个由数组构成的数组传递给 WeakMap 构造器。就像正规 Map 那样,每个内部数组都应当有两个项,第一项是作为键的对象,第二项则是对应的值(任意类型)。例如:
1  | let key1 = {}  | 
对象 key1 与 key2 被用作 Weak Map 的键,get() 与 has() 方法则能访问他们。在传递给 WeakMap 构造器的参数中,若任意键值对使用了非对象的键,构造器就会抛出错误。
Weak Map 的方法
Weak Map 只有两个附加方法能用来与键值对交互。has() 方法用于判断指定的键是否存在于 Map 中,而 delete() 方法则用于移除一个特定的键值对。clear() 方法不存在,这是因为没有必要对键进行枚举,并且枚举 Weak Map 也是不可能的,这与 Weak Set 相同。
对象的私有数据
ES5 中的创建私有数据的方式:
1  | var Person = (function () {  | 
此例用 IIFE 包裹了 Person 的定义,其中含有两个私有属性:privateData 和 privateId。privateData 对象存储了每个实例的私有信息,而 privateId 则被用于为每个实例产生一个唯一ID。当 Person 构造器被调用时,一个不可枚举、不可配置、不可写入的 _id 属性就被添加了。
接下来在 privateData 对象中建立了与实例ID对应的一个入口,其中存储着 name 的值。随后在 getName() 函数中,就能使用 this._id 作为 privateData 的键来提取该值。由于 privateData 无法从 IIFE 外部进行访问,实际的数据就是安全的,尽管 this._id 在 privateData 对象上依然是公开暴露的。
此方式的最大问题在于 privateData 中的数据永远不会消失,因为在对象实例被销毁时没有任何方法可以获知该数据,privateData 对象就将永远包含多余的数据。这个问题现在可以换用 Weak Map 来解决了,如下:
1  | let Person = (function () {  | 
此版本的 Person 范例使用了 Weak Map 而不是对象来保存私有数据。由于 Person 对象的实例本身能被作为键来使用,于是也就无须再记录单独的 ID。当 Person 构造器被调用时,将 this 作为键在 Weak Map 上建立了一个入口,而包含私有信息的对象成为了对应的值,其中只存放了 name 属性。通过将 this 传递给 privateData.get() 方法,以获取值对象并访问其 name 属性,getName() 函数便能提取私有信息。这种技术让私有信息能够保持私有状态,并且当与之关联的对象实例被销毁时,私有信息也会被同时销毁。