五、ES6系列之Set和Map

Set

基本介绍

ES6 提供了新的数据结构 Set,它类似于数组,但是成员的值都是唯一的,没有重复的值。

初始化

Set 本身是一个构造函数,用来生成 Set 数据结构。

1
let set = new Set();

Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

1
2
3
4
5
6
7
8
let set = new Set([1, 2, 3, 4, 4]);
console.log(set); // Set(4) {1, 2, 3, 4}

set = new Set(document.querySelectorAll("div"));
console.log(set.size); // 66

set = new Set(new Set([1, 2, 3, 4]));
console.log(set.size); // 4

上面代码也展示了一种去除数组重复成员的方法。

1
2
// 去除数组的重复成员
[...new Set(array)];

上面的方法也可以用于,去除字符串里面的重复字符。

1
2
[...new Set("ababbc")].join("");
// "abc"

Set 中加入值时,不会发生类型转换。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为 NaN 等于自身,而精确相等运算符认为 NaN 不等于自身,另外,两个对象总是不相等的。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let set = new Set();
set.add(5);
set.add("5");
set; //Set(2) {5, "5"}

set.add(NaN);
set.add(NaN);
set; //Set(3) {5, "5", NaN}

let set2 = new Set();
set2.add({});
set.size; // 1

set2.add({});
set.size; // 2

属性和方法

属性

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是 Set 函数。
  • Set.prototype.size:返回 Set 实例的成员总数。
1
2
3
4
5
let set = new Set();
set.add(1);
set.add(2);
set.size; //2
set.constructor === Set; //true

方法

方法分为操作方法和遍历方法:

操作方法有:

  1. add(value):添加某个值,返回 Set 结构本身。
  2. delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  3. has(value):返回一个布尔值,表示该值是否为 Set 的成员。
  4. clear():清除所有成员,无返回值。

请看示例:

1
2
3
4
5
6
7
let set = new Set();
set.add(1).add(2); // Set(2) {1, 2}
set.delete(2); // true
set.has(2); // false

set.clear(); // undefined
set.has(1); // false

遍历方法有:

  1. keys():返回键名的遍历器
  2. values():返回键值的遍历器
  3. entries():返回键值对的遍历器
  4. forEach():使用回调函数遍历每个成员,无返回值

注意 keys()、values()、entries() 返回的是遍历器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let set = new Set(["red", "green", "blue"]);

for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

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

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

1
2
3
4
5
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + " : " + value));
// 1 : 1
// 4 : 4
// 9 : 9

weakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

  1. WeakSet 的成员只能是对象,而不能是其他类型的值。
1
2
3
4
5
const ws = new WeakSet();
ws.add(1);
// TypeError: Invalid value used in weak set
ws.add(Symbol());
// TypeError: invalid value used in weak set
  1. WeakSet 中的对象都是弱引用。

即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
可以用来保存 DOM 节点,不容易造成内存泄漏,不能遍历。

1
2
3
4
5
6
7
let john = { name: "John" };

let weakSet = new WeakSet();
weakSet.add(john);

john = null; // 覆盖引用
// john 被从内存删除了

WeakSet 结构没有 size 属性,有以下三个方法。:

  • weakSet.add(key)
  • weakSet.delete(key)
  • weakSet.has(key)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const ws = new WeakSet();
const obj = {};
const foo = {};

ws.add(window);
ws.add(obj);

ws.has(window); // true
ws.has(foo); // false

ws.delete(window);
ws.has(window); // false

ws.size; // undefined
ws.forEach; // undefined

ws.forEach(function (item) {
console.log("WeakSet has " + item);
});
// TypeError: undefined is not a function

Map

基本介绍

ES6 提供了新的数据结构 Map 。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

1
2
3
4
5
6
7
8
const m = new Map();
const o = { name: "lilei" };
m.set(o, "content");
m.get(o); //"content"

m.has(o); // true
m.delete(o); // true
m.has(o); // false

作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

1
2
3
4
5
6
7
8
9
10
const map = new Map([
["name", "张三"],
["title", "Author"],
]);

map.size; // 2
map.has("name"); // true
map.get("name"); // "张三"
map.has("title"); // true
map.get("title"); // "Author"

属性和方法

属性

Map 结构的实例有以下属性。

  • Map.prototype.constructor:构造函数,默认就是 Map 函数。
  • Map.prototype.size:返回 Map 实例的成员总数。
1
2
3
4
5
let map = new Map();
map.set(1, "li");
map.set(2, "wang");
map.size; //2
map.constructor === Map; //true

方法

方法分为操作方法和遍历方法:

操作方法有:

  1. set(key, value):设置键名 key 对应的键值为 value,返回 Map 结构本身。
  2. get(key):读取 key 对应的键值,如果找不到 key,返回 undefined。
  3. delete(key):删除某个值,返回一个布尔值,表示删除是否成功。
  4. has(key):返回一个布尔值,表示该值是否为 Map 的成员。
  5. clear():清除所有成员,无返回值。
1
2
3
4
5
6
7
let map = new Map();
map.set(1, "li");
map.set(2, "wang");
map.get(1); // "li"
map.delete(1); // true
map.has(1); // false
map.clear(); // undefined

遍历方法有:

  1. keys():返回键名的遍历器
  2. values():返回键值的遍历器
  3. entries():返回键值对的遍历器
  4. forEach():遍历 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
const map = new Map([
["name", "zhangsan"],
["age", 18],
]);

for (let key of map.keys()) {
console.log(key);
}
// "name"
// "age"

for (let value of map.values()) {
console.log(value);
}
// "zhangsan"
// 18

for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// name zhangsan
// age 18

// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// name zhangsan
// age 18

// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// name zhangsan
// age 18

Map 还有一个 forEach 方法,与数组的 forEach 方法类似,也可以实现遍历。forEach 方法还可以接受第二个参数,用来绑定 this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const map = new Map([
[1, "one"],
[2, "two"],
[3, "three"],
]);
const reporter = {
report: function (key, value) {
console.log("Key: %s, Value: %s", key, value);
},
};

map.forEach(function (value, key, map) {
this.report(key, value);
}, reporter);

weakMap

WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。
WeakMap 与 Map 的区别有两点。

  1. 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。
1
2
3
4
5
6
7
const map = new WeakMap();
map.set(1, 2);
// TypeError: 1 is not an object!
map.set(Symbol(), 2);
// TypeError: Invalid value used as weak map key
map.set(null, 2);
// TypeError: Invalid value used as weak map key
  1. 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的。

即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

1
2
3
4
5
6
const wm = new WeakMap();

const element = document.getElementById("example");

wm.set(element, "some information");
wm.get(element); // "some information"

WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有 keys()、values()和 entries()方法),也没有 size 属性。二是无法清空,即不支持 clear 方法。因此,WeakMap 只有四个方法可用:get()、set()、has()、delete()。

1
2
3
4
5
6
const wm = new WeakMap();

// size、forEach、clear 方法都不存在
wm.size; // undefined
wm.forEach; // undefined
wm.clear; // undefined

例如,我们有用于处理用户访问计数的代码。收集到的信息被存储在 map 中:一个用户对象作为键,其访问次数为值。当一个用户离开时(该用户对象将被垃圾回收机制回收),这时我们就不再需要他的访问次数了。

1
2
3
4
5
6
7
8
9
复制代码;
// visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count

// 递增用户来访次数
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}

参考文献