“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。但是这样做会有三个缺陷。
JavaScript中Object的属性键是String或Symbol,这限制了它们作为不同数据类型的键/值对集合的能力。当然,您可以将其他数据类型强制/字符串化为字符串,但这会增加额外的工作量。
Object不是设计来作为一种数据集合,因此没有有效的方法来确定对象具有多少属性(虽然有Object.keys,但是它很慢)。循环遍历对象的属性时,还会获得其原型属性。您可以将iterable属性添加到所有对象,但不是所有对象都可以用作集合。您可以使用for … in循环和hasOwnProperty()方法,但这只是一种解决方法。循环访问对象的属性时,不一定按照插入的顺序检索属性。
Object具有内置方法,如constructor,toString和valueOf。如果其中一个作为属性添加,则可能导致冲突。虽然您可以使用Object.create(null)来创建一个裸对象(它不从object.prototype继承),但是这只是一个变通方法。
Map对象保存键值对并记住键的原始插入顺序。任何值(对象和原始值)都可以用作键或值.其中key值的对比是基于一种类似于===操作符的算法,不过NaN被认为与自身相等(尽管JS中NaN!==NaN,因此Map中可以使用NaN作为key;
Map中的键值对是有序的,因此在迭代时的顺序与插入时一致。
我们可以轻松创建Map,添加/删除值,迭代访问键/值并有效确定其大小。
1 | // new Map([iterable]) |
Set
Set简单来说就是不包含重复项的值的有序集合,它允许您存储任何类型,无论是原始值还是对象引用,不像数组那样使用索引,Set使用Key访问集合。Set已经存在于Java,Ruby,Python和许多其他语言中。 ES6的Set与其他语言之间的一个区别在于ES6中的Set是有序的。
1 | const planetsOrderFromSun = new Set(); |
弱集合,内存和垃圾回收
JavaScript垃圾回收机制是一种内存管理形式,可以自动删除不再引用的对象并回收其资源。
JS中,当一个对象被引用的时候,往往意味着它正在被使用,或者在将来有可能会被使用。此时对象所占用的内存不会被垃圾回收机制回收掉。Map和Set所引用的对象会被保留,不允许进行垃圾回收。如果Map和Set引用着不再需要的大对象(例如已经从DOM中删除的DOM元素),这可能会消耗大量内存。
为了解决这个问题,ES6还引入了两个名为WeakMap和WeakSet的新弱集合。这些ES6集合是“弱”的,因为它们允许从内存中清除不再需要的对象。
弱引用则可以理解为“引用了对象,但是不影响它的垃圾回收”,举个🌰:
1 | var obj = {}; |
在这个例子中,WeakMap实例wm(弱)引用了obj对象(空对象),接着下方代码释放了对空对象的引用(obj = null),此时和上例一样,空对象将被垃圾回收。也即wm中持有的空对象(弱)引用并不影响对对象本身的垃圾回收。这就是WeakMap中“弱引用”的含义。
WeakMap
WeakMap是一种弱引用key的键值对(key/value)集合,其键(key)必须是一个对象,值(value)可以为任意类型。正由于这样的弱引用,WeakMap的key是无法枚举的 (即无法列举所有的key)。如果key是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果。所以无法使用for…in或者for…of等语句迭代。
WeakMap使用场景
WeakMaps有几个流行的用例。它们可用于保持对象的私有数据私有化,它们还可用于跟踪DOM节点/对象。
场景一
使用WeakMap简化了保持对象数据私有的过程。privateData可以引用Person对象,但不允许在没有特定Person实例的情况下访问,而且随Person实例对象的销毁而消失。
1 | var Person = (function() { |
场景二
使用WeakMap处理事件绑定,在React中如果用到事件监听处理,通常我们必须要在对应的生命周期中相应的进行事件绑定或解除,以防止内存泄漏。然而如果使用WeakMap,我可以不用担心这些问题。
1 | var listeners = new WeakMap(); |
场景三
使用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 | _makeClone() { |
WeakSet
WeakSet是弱引用的Set,当不再需要它们引用的对象时,它们的元素可以被垃圾收集。 WeakSet不允许迭代。
1 | var ws = new WeakSet(); |
WeakSet使用场景
WeakSet使用场景相当有限(至少目前为止)。大多数早期采用者都说WeakSet可用于标记对象而不会改变它们。ES6-Features.org有一个添加和删除WeakSet中元素的示例,以便跟踪对象是否已被标记:
1 | let isMarked = new WeakSet() |