Set集合与Map集合简介

长久以来,数组一直是JavaScript中唯一的集合类型。(不过,有一些开发者认为非数组对象也是集合,只不过是键值对集合,它们的用途与数组完全不同。)JavaScript数组的功能与其他语言中的一样,但在ECMAScript 6标准制定以前,由于可选的集合类型有限,数组使用的又是数值型索引,因而经常被用于创建队列和栈。如果开发者们需要使用非数值型索引,就会用非数组对象创建所需的数据结构,而这就是Set集合与Map集合的早期实现。
Set集合是一种无重复元素的列表,开发者们一般不会逐一读取数组中的元素,也不太可能逐一访问Set集合中的每个元素,通常的做法是检测给定的值在某个集合中是否存在。Map集合内含多组键值对,集合中每个元素分别存放着可访问的键名和它对应的值,Map集合经常被用于缓存频繁取用的数据。在标准正式发布以前,开发者们已经在ECMAScript 5中用非数组对象实现了类似的功能。
ECMAScript 6新标准将Set集合与Map集合添加到JavaScript中,本章将详尽解读这两种新的集合类型。首先,浅析新标准发布前开发者们已经实现的方案,以及这些方案各自的缺陷;然后,讲解这两个集合在ECMAScript 6中的运作原理。

ECMAScript 5中的Set集合与Map集合

在ECMAScript 5中,开发者们经常用类似的方法检查对象的某个属性值是否存在

set: 检查对象中是否存在某个键名

  1. var set = Object.create(null);
  2. set.foo = true;
  3. // 检查属性是否存在
  4. if(set.foo){
  5. // 要执行的代码
  6. }

map:用于获取已存的信息

  1. var map = Object.create(null);
  2. map.foo = "bar";
  3. // 获取已存值
  4. var value = map.foo;
  5. console.log(value); //"bar"

该解决方案的一些问题

所有对象的属性名必须是字符串类型,必须确保每个键名都是字符串类型且在对象中是唯一的

而且内部的自动转换机制会导致很多问题

数值型属性名会自动转换成字符串类型

  1. var map = Object.create(null);
  2. map[5] = "foo";
  3. console.log(map["5"]);//foo

本例中将对象的某个属性赋值为字符串”foo”,所以map[“5”]和map[5]引用的其实是同一个属性。

对象作为属性名会转换成字符串

  1. var map = Object.create(null),
  2. key1 = {},
  3. key2 = {};
  4. map[key1] = "foo";
  5. console.log(map[key1]); //foo
  6. console.log(map[key2]); //foo

由于对象属性的键名必须是字符串,因而这段代码中的key1和key2将被转换为对象对应的默认字符串”[object Object]”,所以map[key2]和map[key1]引用的是同一个属性

注 在JavaScript中有一个in运算符,其不需要读取对象的值就可以判断属性在对象中是否存在,如果存在就返回true。但是,in运算符也会检索对象的原型,只有当对象原型为null时使用这个方法才比较稳妥。

ECMAScript 6中的Set集合

ECMAScript 6中新增的Set类型是一种有序列表,其中含有一些相互独立的非重复值,通过Set集合可以快速访问其中的数据,更有效地追踪各种离散值。

new Set()创建Set集合与set.add()添加元素

调用 new Set()创建Set集合,
调用add()方法向集合中添加元素,
访问集合的size属性可以获取集合中的目前的元素数量

    let set = new Set();
    set.add(5);
    set.add("5");
    set.add(+0);
    set.add(-0);

    console.log(set.size); // 3
    console.log(set); Set(3) // Set(3) {5, "5", 0}

在Set集合中,不会对所存值进行强制的类型转换,数字5和字符串“5”可以作为两个独立元素存在(引擎内部使用第4章介绍的Object.is()方法检测两个值是否一致,唯一例外 +0 和 -0被认为是相等的)

向Set集合中添加多个对象,则它们之间彼此保持独立

由于key1和key2不会被转换成字符串,因而它们在Set集合中是两个独立的元素;

    let set = new Set(),
        key1 = {},
        key2 = {};

    set.add(key1);
    set.add(key2);

    console.log(set); // Set(2) {{…}, {…}}
    console.log(set.size); //2

如果多次调用add()方法并传入相同的值作为参数,那么后续的调用实际上会被忽略:

    let set = new Set();
    set.add(5);
    set.add("5");
    set.add(5); //重复 直接忽略

    console.log(set);   //Set(2) {5, "5"}
    console.log(set.size); //2

注 实际上,Set构造函数可以接受所有可迭代对象作为参数,数组、Set集合、Map集合都是可迭代的,因而都可以作为Set构造函数的参数使用;构造函数通过迭代器从参数中提取值。第8章将详细讲解可迭代协议和迭代器协议。

set.has()检测Set集合中是否存在某个值

通过has()方法可以检测Set集合中是否存在某个值:

    let set = new Set();
    set.add(5);
    set.add("5");

    console.log(set.has(5)); // true
    console.log(set.has(6)); // false

set.delete()移除元素与set.clear()移除所有元素

    let set = new Set();
    set.add(5);
    set.add("5");

    console.log(set.has(5)); //true

    console.log(set.delete(5)); //true

    set.delete(5);

    console.log(set.has(5)); //false

    console.log(set.size); //1

    set.clear();

    console.log(set.has("5")); //false

    console.log(set.size); //0

Set集合的forEach()方法

上面代码说明,forEach方法的参数就是一个处理函数。该函数的参数与数组的forEach一致,依次为键值、键名、集合本身(上例省略了该参数)。这里需要注意,Set 结构的键名就是键值(两者是同一个值),因此第一个参数与第二个参数的值永远都是一样的(Set集合没有键名)。

    let set = new Set([1, 4, 6]);

    set.forEach((value, key, ownerSet) => {
        console.log(key + " " + value)
        console.log(ownerSet === set)
    })
// 1 1
// true
// 4 4
// true
// 6 6
// true

另外,forEach方法还可以有第二个参数,表示绑定处理函数内部的this对象。

    let set = new Set([1, 2]);
    let processor = {
        output(value) {
            console.log(value);
        },
        process(dataSet) {
            dataSet.forEach(function (value) {
                this.output(value);
            }, this);
        }
    }
    processor.process(set);
//1
//2

也可以用箭头函数 则无需指定this

    let set = new Set([1, 2]);
    let processor = {
        output(value) {
            console.log(value);
        },
        process(dataSet) {
            dataSet.forEach(value => { this.output(value) };
        }
    }
    processor.process(set);
//1
//2

Set集合转数组

数组转Set集合

new Set() 参数为数组

Set集合转数组

展开运算符 ...

数组去重

    let set = new Set([1, 2, 3, 3, 3, 4, 5]),
        array = [...set];

    console.log(array); // [1, 2, 3, 4, 5]

//合并写法
    let set = [...new Set([1, 2, 3, 3, 3, 4, 5])];

    console.log(set); // [1, 2, 3, 4, 5]

封装函数

    function eliminateDuplicates(items) {
        return [...new Set(items)];
    }

    let numbers = [1, 2, 3, 3, 3, 4, 5];
    console.log(eliminateDuplicates(numbers)); //[1, 2, 3, 4, 5]

Weak Set集合

将对象存储在Set的实例与存储在变量中完全一样,只要Set实例中的引用存在,垃圾回收机制就不能释放该对象的内存空间,于是之前提到的Set类型可以被看作是一个强引用的Set集合。举个例子:

    let set = new Set(),
        key = {};

    set.add(key);
    console.log(set.size); //1

    key = null;

    console.log(set.size); //1

    //重新取回原始引用

    key = [...set][0]; // {}

在这个示例中,将变量key设置为null时便清除了对初始对象的引用,但是Set集合却保留了这个引用,你仍然可以使用展开运算符将Set集合转换成数组格式并从数组的首个元素取出该引用。大部分情况下这段代码运行良好,但有时候你会希望当其他所有引用都不再存在时,让Set集合中的这些引用随之消失。举个例子,如果你在Web页面中通过JavaScript代码记录了一些DOM元素,这些元素有可能被另一段脚本移除,而你又不希望自己的代码保留这些DOM元素的最后一个引用。(Weak Set的使用场景)(这个情景被称作内存泄露。)
为了解决这个问题,ECMAScript 6中引入了另外一个类型:Weak Set集合(弱引用Set集合)。Weak Set集合只存储对象的弱引用,并且不可以存储原始值;集合中的弱引用如果是对象唯一的引用,则会被回收并释放相应内存。

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。

const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set

上面代码试图向 WeakSet 添加一个数值和Symbol值,结果报错,因为 WeakSet 只能放置对象。
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

创建Weak Set集合 添加add() 检测has() 移除方法delete()

    let set = new WeakSet(),
        key = {};

    console.log(set); // WeakSet {}
    set.add(key);

    console.log(set.has(key)); //true

    set.delete(key);

    console.log(set.has(key)); //false

Weak Set集合的使用方式与Set集合类似,可以向集合中添加引用,从中移除引用,也可以检查集合中是否存在指定对象的引用。也可以调用WeakSet构造函数并传入一个可迭代对象来创建Weak Set集合:

    let key1 = {},
        key2 = {},
        set = new WeakSet([key1, key2]);

    console.log(set.has(key1)); //true
    console.log(set.has(key2)); //true

在这个示例中,向WeakSet构造函数传入一个含有两个对象的数组,最终创建一个包含这两个对象的Weak Set集合。请记住,WeakSet构造函数不接受任何原始值,如果数组中包含其他非对象值,程序会抛出错误。

两种Set类型的主要区别

两种Set类型之间最大的区别是Weak Set保存的是对象值的弱引用,下面这个示例将展示二者间的差异:

    let set = new WeakSet(),
        key = {};

    //向集合Set中添加对象

    console.log(set.has(key)); //true

    //移除对象key的最后一个强引用(we
    key = null;

以上示例展示了一些Weak Set集合与普通Set集合的共同特性,但是它们之间还有下面几个差别:
· 在WeakSet的实例中,如果向add()、has()和delete()这3个方法传入非对象参数都会导致程序报错。
· Weak Set集合不可迭代,所以不能被用于for-of循环。
· Weak Set集合不暴露任何迭代器(例如keys()和values()方法),所以无法通过程序本身来检测其中的内容。
· Weak Set集合不支持forEach()方法。
· Weak Set集合不支持size属性。

ECMAScript 6中的Map集合

ECMAScript 6中的Map类型是一种储存着许多键值对的有序列表,其中的键名和对应的值支持所有的数据类型。键名的等价性判断是通过调用Object.is()方法实现的,所以数字5与字符串“5”会被判定为两种类型,可以分别作为独立的两个键出现在程序中,这一点与对象中不太一样,因为对象的属性名总会被强制转换成字符串类型。

set()添加元素、get()获取元素

    let map = new Map();
    map.set("title", "es6");
    map.set("year", 2016);

    console.log(map.get("title")); //es6
    console.log(map.get("year")); //2016

在对象中,无法用对象作为对象属性的键名;但是在Map集合中,却可以这样做:

    let map = new Map(),
        key1 = {},
        key2 = {};

    map.set(key1, 5);
    map.set(key2, 42);

    console.log(map.get(key1)); //5
    console.log(map.get(key2)); //42

Map集合支持的方法


检测指定的键名是否存在has()
从Map集合中移除指定键名其应对的值delete()
移除Map集合的所有键值对clear()
size属性 Map集合中键值对的数量

    let map = new Map();
    map.set("name", "annm");
    map.set("age", 25);

    console.log(map.size); //2

    console.log(map.has("name")); //true
    console.log(map.get("name")); //annm

    console.log(map.has("age")); //25
    console.log(map.get("age")); //true

    map.delete("name");
    console.log(map.has("name")); //false
    console.log(map.get("name")); //undefined
    console.log(map.size); //1

    map.clear();
    console.log(map.has("name")); //false
    console.log(map.get("name")); //undefined
    console.log(map.has("age")); //false
    console.log(map.get("age")); //undefined
    console.log(map.size); //0

Map集合的初始化

    let map = new Map([["name", "annm"], ["age", 25]]);

    console.log(map.has("name"));//true
    console.log(map.get("name"));//annm
    console.log(map.has("age"));//true
    console.log(map.get("age"));//25
    console.log(map.size);//2
    console.log(map); //Map(2) {"name" => "annm", "age" => 25}

Map集合的forEach()方法

回调函数可传3个参数: 值、键名、Map集合本身
第二个参数作为回调函数的this值

    let map = new Map([["name", "annm"], ["age", 25]]);

    map.forEach((value, key, ownerMap) => {
        console.log(key + " " + value);
        console.log(map === ownerMap);
    })
     //name annm
     //true
     //age 25
     //true

Weak Map 集合

Weak Set是弱引用Set集合,相对的,Weak Map是弱引用Map集合,也用于存储对象的弱引用。Weak Map集合中的键名必须是一个对象,如果使用非对象键名会报错;集合中保存的是这些对象的弱引用,如果在弱引用之外不存在其他的强引用,引擎的垃圾回收机制会自动回收这个对象,同时也会移除Weak Map集合中的键值对。但是只有集合的键名遵从这个规则,键名对应的值如果是一个对象,则保存的是对象的强引用,不会触发垃圾回收机制。
Weak Map集合最大的用途是保存Web页面中的DOM元素,例如,一些为Web页面打造的JavaScript库,会通过自定义的对象保存每一个引用的DOM元素。
使用这种方法最困难的是,一旦从Web页面中移除保存过的DOM元素,如何通过库本身将这些对象从集合中清除;否则,可能由于库过于庞大而导致内存泄露,最终程序不再正常执行。如果用WeakMap集合来跟踪DOM元素,这些库仍然可以通过自定义的对象整合每一个DOM元素,而且当DOM元素消失时,可以自动销毁集合中的相关对象。

使用Weak Map集合

set()方法添加数据 get()方法获取数据

    let map = new WeakMap(),
        element = document.querySelector(".element");
    map.set(element, "Original");

    let value = map.get(element);
    console.log(value); //Original

    //移除element元素

    element.parentNode.removeChild(element);

    element = null;

    //此时Weak Map 集合为空

在这个示例中储存了一个键值对,键名element是一个DOM元素,其对应的值是一个字符串,将DOM元素传入get()方法即可获取之前存过的值。如果随后从document对象中移除DOM元素并将引用这个元素的变量设置为null,那么Weak Map集合中的数据也会被同步清除。

Weak Map集合的初始化方法

    let key1 = {},
        key2 = {},
        map = new WeakMap([[key1, "Hello"], [key2, 42]]);

    console.log(map.has(key1));
    console.log(map.get(ket1));
    console.log(map.has(key2));
    console.log(map.get(key2));

Wask Map集合支持的方法

Weak Map集合只支持两个可以操作键值对的方法:has()方法可以检测给定的键在集合中是否存在;delete()方法可以移除指定的键值对。

    let map = new WeakMap(),
        element = document.querySelector(".element");

    map.set(element, "Original");

    console.log(map.has(element)); //true
    console.log(map.get(element)); //"Original"

    map.delete(element);
    console.log(map.has(element)); //false
    console.log(map.get(element)); //undefined

私有对象属性

Weak Map集合其中的一个实际应用是存储对象实例的私有数据。在ECMAScript 6中对象的所有属性都是公开的,如果想要储存一些只对对象开放的数据,则需要一些创造力,请看以下这个示例:

    function Person(name) {
        this._name = name;
    }
    Person.prototype.getName = function () {
        return this._name;
    }

在这段代码中,约定前缀为下划线_的属性为私有属性,不允许在对象实例外改变这些属性。例如,只能通过getName()方法读取this._name属性,不允许改变它的值。然而没有任何标准规定如何写_name属性,所以它也有可能在无意间被覆写。

Weak Map解法

        let Person = (function () {
            let privateData = new WeakMap();
            function Person(name) {
                privateData.set(this, { name: name });
            }
            Person.prototype.getName = function () {
                return privateData.get(this).name;
            };
            return Person;
        }());

        const a = new Person('y');

        console.log(a.getName());

        console.log(a);

经过改进后的Person构造函数选用一个Weak Map集合来存放私有数据。由于Person对象的实例可以直接作为集合的键使用,无须单独维护一套ID的体系来跟踪数据。调用Person构造函数时,新条目会被添加到Weak Map集合中,条目的键是this,值是对象包含的私有信息,在这个示例中,值是一个包含name属性的对象。调用getName()函数时会将this传入privateData.get()方法作为参数获取私有信息,亦即获取value对象并且访问name属性。只要对象实例被销毁,相关信息也会被销毁,从而保证了信息的私有性。

Weak Map集合的使用方式及使用限制

当你要在Weak Map集合与普通的Map集合之间做出选择时,需要考虑的主要问题是,是否只用对象作为集合的键名。如果是,那么Weak Map集合是最好的选择。当数据再也不可访问后集合中存储的相关引用和数据都会被自动回收,这有效地避免了内存泄露的问题,从而优化了内存的使用。请记住,相对Map集合而言,Weak Map集合对用户的可见度更低,其不支持通过forEach()方法、size属性及clear()方法来管理集合中的元素。如果你非常需要这些特性,那么Map集合是一个更好的选择,只是一定要留意内存的使用情况。当然,如果你只想使用非对象作为键名,那么普通的Map集合是你唯一的选择。

小结

ECMAScript 6正式将Set集合与Map集合引入到JavaScript中,而在这之前,开发者们经常用对象来模拟这两种集合,但是由于对象属性自身的限制,经常会遇到一些问题。
Set集合是一种包含多个非重复值的无序列表,值与值之间的等价性是通过Object.is()方法来判断的,如果相同,则会自动过滤重复的值,所以可以用Set集合来过滤数组中的重复元素。Set集合不是数组的子类,所以你不能随机访问集合中的值,只能通过has()方法检测指定的值是否存在于Set集合中,或者通过size属性查看Set集合中的值的数量。Set类型同样支持forEach()方法来处理集合中的每一个值。
Weak Set集合是一类特殊的Set集合,集合只支持存放对象的弱引用,当该对象的其他强引用都被清除时,集合中的弱引用也会自动被垃圾回收。由于内存管理非常复杂,Weak Set集合不可以被检查,因此追踪成组的对象是该集合最好的使用方式。
Map是多个无序键值对组成的集合,键名支持任意数据类型。与Set集合相似的是,Map集合也是通过Object.is()方法来过滤重复值,数字5和字符串“5”可以分别作为两个独立的键名使用。通过set()方法可以将任意类型的值添加到集合中,通过get()方法可以检索集合中的所有值,通过size属性可以检查集合中包含的值的数量,通过forEach()方法可以遍历并操作集合中的每一个值。
Weak Map集合是一类特殊的Map集合,只支持对象类型的键名。与Weak Set相似的是,集合中存放的键是对象的弱引用,当该对象的其他强引用都被清除时,集合中弱引用键及其对应的值也会自动被垃圾回收。这种内存管理机制非常适合这样的场景:为那些实际使用与生命周期管理分离的对象添加额外信息。