Set 与 Map


Set 与 Map 这类数据结构,在程序设计中经常会使用到,ES6 中正式规范了SetMapAPI,虽然还不是完善,然而在某些需求时,确实可以省一些功夫。

首先来看到Set,它内部的元素不重复:

> let set = new Set([1, 2, 3, 4, 5, 1, 2, 3]);
undefined
> set;
Set { 1, 2, 3, 4, 5 }
> set.add(6);
Set { 1, 2, 3, 4, 5, 6 }
> set.has(3);
true
> set.forEach(elem => console.log(elem));
1
2
3
4
5
6
undefined
> for(let elem of set) {
...     console.log(elem);
... }
1
2
3
4
5
6
undefined
> [...set];
[ 1, 2, 3, 4, 5, 6 ]
> let [a, b, c] = set;
undefined
> a;
1
> b;
2
> c;
3
>

Set本身是可迭代的,因此可以使用for...of,当然,也可以适用...来 Spread 元素,或者解构语法。

Set是无序的,因此没有直接可取值的 get 之类的方法,除了上面方法示范之外,Set还有delete可用来删除元素,clear可用来清空元素,size可用来查看Set中的元素数量。

那么问题来了,Set中判定元素是否重复的依据是什么?如果是基本类型,显然就是值是不是相同,这没有问题,那么对象呢?

> let o1 = {x: 10};
undefined
> let o2 = {x: 10};
undefined
> let o3 = o2;
undefined
> let set = new Set([o1, o2, o3]);
undefined
> set;
Set { { x: 10 }, { x: 10 } }
>

在上面的例子中,最后的set有两个对象,显然地,并不是判定对象的特性实际上是否相等,那么有 equals、hashCode 之类的协定,可以定义对象实质的内含值是否相同吗?没有!对于对象,基本上就是相当于===比较。

因为 JavaScript 的对象特性很容易变更,如果你了解其他语言中 equals、hashCode 之类的协定,也应该知道,一个状态可变的对象,在实现 equals、hashCode 之类的协定时会有许多坑,因此就目前来说,Set是特意这么做的(并不是忽略了),这会造成一些不便,如果你真的需要能依 equals、hashCode 之类的协定来判定对象相等性,那必须自行实现或者是寻求既有的程序库。

对于Set来说,NaN是可以判定相等的:

> let set = new Set([NaN, NaN, 0, -0]);
undefined
> set;
Set { NaN, 0 }
>

之后谈到 ECMAScript 6 的相等性判定时,会看到像Object.is会将 0 与 -0 视为不相等,然而,对于 0、-0,Set认定是相等的,具体来说,Set是采用所谓的 SameValueZero 演算来判定相等性,详情会在下一篇文件中说明。

在谈到 ES6 的Set时,通常会一并谈到WeakSet,简单来说,垃圾收集时不会考虑对象是否被WeakSet包含着,只要对象没有其他名称参考着,就会回收对象,如果WeakSet中本来有该对象,会自动消失,这可以用来避免必须使用Set管理对象,而后忘了从Set中清除而发生内存泄漏的问题。

WeakSet中的元素只能是对象,不能是numberbooleanstringsymbol等,也不能是null,由于对象可能被垃圾回收,因此它不能被迭代(也不能使用forEach)、不能使用sizeclear方法,只能使用addhasdelete方法。

接着来谈谈Map,虽然 JavaScript 中的对象,本身就是键与值的集合体,然而,键的部份基本上就是字符串,ES6 中多了个Symbol可以做为特性,除此之外,就算使用[]指定对象作为键,它会获取字符串描述作为特性:

> let o = {x: 10};
undefined
> let map = {
...     [o] : 'foo'
... };
undefined
> for(let p in map) {
...     console.log(p);
... }
[object Object]
undefined
>

在 ES6 中,Map的键可以是对象:

> let o = {x: 10};
undefined
> let map = new Map();
undefined
> map.set(o, 'foo');
Map { { x: 10 } => 'foo' }
> map.set({y : 10}, 'foo2');
Map { { x: 10 } => 'foo', { y: 10 } => 'foo2' }
> for(let [key, value] of map) {
...     console.log(key, value);
... }
{ x: 10 } 'foo'
{ y: 10 } 'foo2'
undefined
> map.get(o);
'foo'
> map.delete(o);
true
> map;
Map { { y: 10 } => 'foo2' }
>

Map却使用set方法?怪怪的!….

Map本身可迭代,使用for...of的话,迭代出来的元素会是个包含键与值的对象,也可以使用...来解构。除了以上示范的方法之外,可以使用has判定是否具有某个键,delete删除某键(与对应的值),使用clear清空Map,使用keys获取全部的键,使用values获取全部的值,使用entries获取键值对,使用size获取键值数量,也可以使用forEach等。

构造Map时,可以使用数组,其中必须是[[键, 值], [键, 值]]的形式:

> let map = new Map([['k1', 'v1'], ['k2', 'v2']]);
undefined
> map;
Map { 'k1' => 'v1', 'k2' => 'v2' }
>

Map中的键必须是唯一的,判定的方式是 SameValueZero。

在谈到 ES6 的Map时,通常会一并谈到WeakMap,简单来说,垃圾收集时不会考虑对象是否被WeakMap作为键,只要对象没有其他名称参考着,就会回收对象,如果WeakMap中本来有该对象作为键,会自动消失,这可以用来避免必须使用Map管理对象,而后忘了从Map中清除而发生内存泄漏的问题。

WeakMap中的键只能是对象,不能是numberbooleanstringsymbol等,也不能是null,由于键对象可能被垃圾回收,因此它不能被迭代(也不能使用forEach)、不能使用sizeclear方法,只能使用getsethasdelete方法。


展开阅读全文