Set、Map、WeakSet、WeakMap的区别

“Good programmers use their brains, but good guidelines save us having to think out every case.”—Francis Glassborow

前言

大多数主流编程语言都有多种内置的数据集合。例如Python拥有列表(list)、元组(tuple)和字典(dictionary),Java有列表(list)、集合(set)、队列(queue)。然而JavaScript直到ES6的发布之前,只拥有数组(array)和对象(object)这两个内建的数据集合。ES6的出现,引入了诸如Map、Set、WeakeMap、WeakMap等新的数据结构为这门语言注入了新的能量和活力。

HashMap,Dictionary等数据结构是各种编程语言存储键/值对的几种方式,这些数据结构针对快速检索进行了优化。当然ES6中的Map、Set、WeakeMap、WeakMap这些数据结构底层都是通过(hash tables)散列表实现的

Map

在ES5中,我们通常使用内置的Object(它们只是具有键和值的属性的任意集合)模拟Map。但是这样做会有三个缺陷

  1. JavaScript中Object的属性键是String或Symbol,这限制了它们作为不同数据类型的键/值对集合的能力。当然,您可以将其他数据类型强制/字符串化为字符串,但这会增加额外的工作量。

  2. Object不是设计来作为一种数据集合,因此没有有效的方法来确定对象具有多少属性(虽然有Object.keys,但是它很慢)。循环遍历对象的属性时,还会获得其原型属性。您可以将iterable属性添加到所有对象,但不是所有对象都可以用作集合。您可以使用for … in循环和hasOwnProperty()方法,但这只是一种解决方法。循环访问对象的属性时,不一定按照插入的顺序检索属性。

  3. Object具有内置方法,如constructor,toString和valueOf。如果其中一个作为属性添加,则可能导致冲突。虽然您可以使用Object.create(null)来创建一个裸对象(它不从object.prototype继承),但是这只是一个变通方法。

MDN对Map定义:

Map对象保存键值对并记住键的原始插入顺序。任何值(对象和原始值)都可以用作键或值.其中key值的对比是基于一种类似于===操作符的算法,不过NaN被认为与自身相等(尽管JS中NaN!==NaN,因此Map中可以使用NaN作为key;
 Map中的键值对是有序的,因此在迭代时的顺序与插入时一致。

我们可以轻松创建Map,添加/删除值,迭代访问键/值并有效确定其大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// new Map([iterable])
let recipeMap = new Map([
['Cucumber', '500 gr'],
['Tomatoes', '350 gr'],
]);

// Sets the value for the key in the Map object. Returns the Map object.
recipeMap = recipeMap.set('Sour cream', '50 gr');

// Returns a boolean asserting whether a value has been associated to the key in the Map object or not.
console.log(recipeMap.has('Cucumber')); // true

// loop by keys
for(let fruit of recipeMap.keys()) {
console.log(fruit);
// Cucumber
// Tomatoes
// Sour cream
}

// loop by values [key, value]
for(let amount of recipeMap.values()) {
console.log(amount);
// 500 gr
// 350 gr
// 50 gr
}

// loop by recoeds
for(let entry of recipeMap) { // same like recipeMap.entries()
console.log(entry);
// ["Cucumber", "500 gr"]
// ["Tomatoes", "350 gr"]
// ["Sour cream", "50 gr"]
}

// Returns true if an element in the Map object existed and has been removed, or false if the element does not exist.
console.log(recipeMap.delete('paopaolee')); // false

// Removes all key/value pairs from the Map object.
recipeMap.clear();

// Returns the number of key/value pairs in the Map object.
console.log(recipeMap.size === 0); // True

Set

MDN对Map定义:

Set简单来说就是不包含重复项的值的有序集合,它允许您存储任何类型,无论是原始值还是对象引用,不像数组那样使用索引,Set使用Key访问集合。Set已经存在于Java,Ruby,Python和许多其他语言中。 ES6的Set与其他语言之间的一个区别在于ES6中的Set是有序的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const planetsOrderFromSun = new Set();
planetsOrderFromSun.add('Mercury');
planetsOrderFromSun.add('Venus').add('Earth').add('Mars'); // Chainable Method
console.log(planetsOrderFromSun.has('Earth')); // True

planetsOrderFromSun.delete('Mars');
console.log(planetsOrderFromSun.has('Mars')); // False

for (const x of planetsOrderFromSun) {
console.log(x); // Same order in as out - Mercury Venus Earth
}
console.log(planetsOrderFromSun.size); // 3

planetsOrderFromSun.add('Venus'); // Trying to add a duplicate
console.log(planetsOrderFromSun.size); // Still 3, Did not add the duplicate

planetsOrderFromSun.clear();
console.log(planetsOrderFromSun.size); // 0

弱集合,内存和垃圾回收

JavaScript垃圾回收机制是一种内存管理形式,可以自动删除不再引用的对象并回收其资源。

JS中,当一个对象被引用的时候,往往意味着它正在被使用,或者在将来有可能会被使用。此时对象所占用的内存不会被垃圾回收机制回收掉。Map和Set所引用的对象会被保留,不允许进行垃圾回收。如果Map和Set引用着不再需要的大对象(例如已经从DOM中删除的DOM元素),这可能会消耗大量内存。

  为了解决这个问题,ES6还引入了两个名为WeakMap和WeakSet的新弱集合。这些ES6集合是“弱”的,因为它们允许从内存中清除不再需要的对象。

 弱引用则可以理解为“引用了对象,但是不影响它的垃圾回收”,举个🌰:

1
2
3
4
5
6
7
var obj = {};
var wm = new WeakMap();
wm.set(obj, 1);
wm.get(obj); // 1
......
obj = null;
wm.get(obj); // 这句没有意义

在这个例子中,WeakMap实例wm(弱)引用了obj对象(空对象),接着下方代码释放了对空对象的引用(obj = null),此时和上例一样,空对象将被垃圾回收。也即wm中持有的空对象(弱)引用并不影响对对象本身的垃圾回收。这就是WeakMap中“弱引用”的含义。

WeakMap

MDN对Map定义:

WeakMap是一种弱引用key的键值对(key/value)集合,其键(key)必须是一个对象,值(value)可以为任意类型。正由于这样的弱引用,WeakMap的key是无法枚举的 (即无法列举所有的key)。如果key是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果。所以无法使用for…in或者for…of等语句迭代。

WeakMap使用场景

 WeakMaps有几个流行的用例。它们可用于保持对象的私有数据私有化,它们还可用于跟踪DOM节点/对象。

场景一

使用WeakMap简化了保持对象数据私有的过程。privateData可以引用Person对象,但不允许在没有特定Person实例的情况下访问,而且随Person实例对象的销毁而消失。

1
2
3
4
5
6
7
8
9
10
11
12
var Person = (function() {
var privateData = new WeakMap();

function Person(name) {
privateData.set(this, { name: name });
}

Person.prototype.getName = function() {
return privateData.get(this).name; // 只能通过Person实例访问对应的name
};
return Person();
}());

场景二

使用WeakMap处理事件绑定,在React中如果用到事件监听处理,通常我们必须要在对应的生命周期中相应的进行事件绑定或解除,以防止内存泄漏。然而如果使用WeakMap,我可以不用担心这些问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var listeners = new WeakMap();
// 监听事件
function on(object, event, fn){
var thisListeners = listeners.get(object);
if(!thisListeners) thisListeners = {};
if(!thisListeners[event]) thisListeners[event] = [];
thisListeners[event].push(fn);
listeners.set(object, thisListeners);
}
// 触发事件
function emit(object, event){
var thisListeners = listeners.get(object);
if(!thisListeners) thisListeners = {};
if(!thisListeners[event]) thisListeners[event] = [];
thisListeners[event].forEach(function(fn){
fn.call(object, event);
});
}
// 使用
var obj = {};
on(obj, 'hello', function(){
console.log('hello');
});
emit(obj, 'hello');

场景三

使用WeakMap跟踪DOM节点编辑,删除和更改。例如,Google的Polymer项目在一段名为PositionWalker的代码中使用了WeakMap

PositionWalker keeps track of a position within a DOM subtree, as a current node and an offset within that node.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_makeClone() {
this._containerClone = this.container.cloneNode(true);
this._cloneToNodes = new WeakMap();
this._nodesToClones = new WeakMap();

...

let n = this.container;
let c = this._containerClone;

// find the currentNode's clone
while (n !== null) {
if (n === this.currentNode) {
this._currentNodeClone = c;
}
this._cloneToNodes.set(c, n);
this._nodesToClones.set(n, c);

n = iterator.nextNode();
c = cloneIterator.nextNode();
}
}

WeakSet

MDN对Map定义:

WeakSet是弱引用的Set,当不再需要它们引用的对象时,它们的元素可以被垃圾收集。 WeakSet不允许迭代。

1
2
3
4
5
6
7
8
9
10
11
12
var ws = new WeakSet();
var foo = {};
var bar = {};

ws.add(foo);
ws.add(bar);

ws.has(foo); // true
ws.has(bar); // true

ws.delete(foo); // removes foo from the set
ws.has(foo); // false, foo has been removed

WeakSet使用场景

WeakSet使用场景相当有限(至少目前为止)。大多数早期采用者都说WeakSet可用于标记对象而不会改变它们。ES6-Features.org有一个添加和删除WeakSet中元素的示例,以便跟踪对象是否已被标记:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let isMarked     = new WeakSet()
let attachedData = new WeakMap()

export class Node{
constructor(id){
this.id = id;
}
mark(){
isMarked.add(this);
}
unmark(){
isMarked.delete(this);
}
marked(){
return isMarked.has(this);
}
set data(data){
attachedData.set(this, data);
}
get data(){
return attachedData.get(this);
}
}

let foo = new Node("foo")

JSON.stringify(foo) === '{"id":"foo"}'
foo.mark()
foo.data = "bar"
foo.data === "bar"
JSON.stringify(foo) === '{"id":"foo"}'

isMarked.has(foo) === true
attachedData.has(foo) === true
foo = null /* remove only reference to foo */
attachedData.has(foo) === false
isMarked.has(foo) === false

参考文章