添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

前言

大多数主流编程语言都有多种内置的数据集合。例如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继承),但是这只是一个变通方法。

    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
  • ES6 Collections: Using Map, Set, WeakMap, WeakSet
  • ES2015 WeakMap的学习和使用
  • JavaScript (ES-2015) Set, Map, WeakSet and WeakMap
  • V8: Optimizing hash tables: hiding the hash code
  •