9.set和map
set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set
与数组之间的最大区别是:
Set
不基于索引,不能根据集合中的条目在集合中的位置引用这些条目Set
中的条目不能单独被访问
创建set
Set
本身是一个构造函数,用来生成 Set
数据结构。
const games = new Set();
console.log(games); //Set{}
此代码会创建空的 Set games,其中没有条目。
Set
函数也可以接受一个数组(或者具有 iterable
接口的其他数据结构)作为参数,用来初始化。
const set = new Set([1,2,3,3,4])
[...set] //[1, 2, 3, 4]
set.size //4
for (let i of s) {
console.log(i);
} //1, 2, 3, 4
const set = new Set('hello')
[...set] // ["h", "e", "l", "o"]
修改 Set
创建 Set 后,你可能想要添加或删除条目。如何操作呢?可以使用名称对应的 .add() 和 .delete()
方法:
const set = new Set([1,2,3,3,4])
set.add(5)
set.add('hello')
set.delete(1)
// Set{1, 2, 3, 4, 5,'hello'}
另一方面,如果你想要删除 Set 中的所有条目,可以使用 .clear() 方法。
set()
console.log(set); //Set{}
提示:
-
如果你尝试向
Set
中.add()
重复的条目,系统不会报错,但是该条目不会添加到 Set 中。此外,如果你尝试 .delete() Set 中不存在的条目,也不会报错,Set 保持不变。 -
.add()
添加不管成功与否,都会返回该Set
对象。另一方面,.delete()
则会返回一个布尔值,该值取决于是否成功删除(即如果该元素存在,返回true
,否则返回false
)。
使用 Set
查看长度
使用 .size
属性可以返回 Set
中的条目数:
const months = new Set(['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);
console.log(months.size); //12
注意,不能像数组那样通过索引访问 Set
,因此要使用 .size
属性,而不是 .length
属性来获取 Set
的大小。
检查是否存在某个条目
使用 .has()
方法可以检查 Set
中是否存在某个条目。如果 Set
中有该条目,则 .has()
将返回 true
。如果 Set
中不存在该条目,则 .has()
将返回 false
。
months.has('September') //true
检索所有值
Set 结构的实例有四个遍历方法,可以用于遍历成员。
keys()
:返回键名的遍历器values()
:返回键值的遍历器entries()
:返回键值对的遍历器forEach()
:使用回调函数遍历每个成员
keys
方法、values
方法、entries
方法返回的都是遍历器对象,由于 Set
结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys
方法和values
方法的行为完全一致。
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"]
上面代码中,entries
方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。
Set
结构的实例与数组一样,也拥有forEach
方法,用于对每个成员执行某种操作,没有返回值。
set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
WeakSet
WeakSet
和普通 Set
很像,但是具有以下关键区别:
- WeakSet 只能包含对象
- WeakSet 无法迭代,意味着不能循环访问其中的对象
- WeakSet 没有 .clear() 方法
const student1 = { name: 'James', age: 26, gender: 'male' };
const student2 = { name: 'Julia', age: 27, gender: 'female' };
const student3 = { name: 'Richard', age: 31, gender: 'male' };
const roster = new WeakSet([student1, student2, student3]);
console.log(roster);
//WeakSet {Object {name: 'Julia', age: 27, gender: 'female'}, Object {name: 'Richard', age: 31, gender: 'male'}, Object {name: 'James', age: 26, gender: 'male'}}
但是如果你尝试添加对象以外的内容,系统将报错!
roster.add('Amanda'); //Uncaught TypeError: Invalid value used in weak set(…)
其次,WeakSet
中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet
对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet
之中。
这是因为垃圾回收机制依赖引用计数,如果一个值的引用次数不为0
,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet
里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet
适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet
里面的引用就会自动消失。
由于上面这个特点,WeakSet
的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet
内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6
规定 WeakSet
不可遍历。
这些特点同样适用于后面要介绍的 WeakMap
结构。
WeakSet
结构有以下三个方法。
- WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
- WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
- WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
Map
如果说 Set 类似于数组,那么 Map 就类似于对象,因为 Map 存储键值对,和对象包含命名属性及值相类似。
本质上,Map
是一个可以存储键值对的对象,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
创建 Map
要创建 Map,只需输入
const employees = new Map();
console.log(employees); //Map {}
这样就会创建空的 Map employee
,没有键值对。
修改 Map
和 Set 不同,你无法使用值列表创建 Map;而是使用 Map 的 .set() 方法添加键值。
.set()
方法有两个参数。第一个参数是键,用来引用第二个参数,即值。
要移除键值对,只需使用 .delete()
方法。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
m.clear() //Map {}
同样,和 Set
类似,你可以使用 .clear()
方法从 Map
中删除所有键值对。
提示:如果你使用 .set()
向 Map
中添加键已存在的键值对,不会收到错误,但是该键值对将覆盖 Map
中的现有键值对。此外,如果尝试使用 .delete()
删除 Map
中不存在的键值,不会收到错误,而 Map
会保持不变。
如果成功地删除了键值对,.delete()
方法会返回 true
,失败则返回 false
。.set()
如果成功执行,则返回 Map
对象本身。
Map
也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
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"
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
事实上,不仅仅是数组,任何具有 Iterator
接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数。这就是说,Set
和Map
都可以用来生成新的 Map
。
注意,只有对同一个对象的引用,Map
结构才将其视为同一个键。这一点要非常小心。
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
上面代码的set
和get
方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get
方法无法读取该键,返回undefined
。
遍历方法
Map
结构原生提供三个遍历器生成函数和一个遍历方法。
- keys():返回键名的遍历器。
- values():返回键值的遍历器。
- entries():返回所有成员的遍历器。
- forEach():遍历 Map 的所有成员。
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
上面代码最后的那个例子,表示Map
结构的默认遍历器接口(Symbol.iterator
属性),就是entries
方法。
map[Symbol.iterator] === map.entries
// true
Map
结构转为数组结构,比较快速的方法是使用扩展运算符(...)。
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
WeakMap
WeakMap
和普通 Map
很像,但是具有以下关键区别:
- WeakMap 只能包含对象作为键,
- WeakMap 无法迭代,意味着无法循环访问,并且
- WeakMap 没有 .clear() 方法。
- WeakMap的键名所指向的对象,不计入垃圾回收机制。
你可以像创建普通 Map 那样创建 WeakMap,但是需要使用 WeakMap 构造函数。
const book1 = { title: 'Pride and Prejudice', author: 'Jane Austen' };
const book2 = { title: 'The Catcher in the Rye', author: 'J.D. Salinger' };
const book3 = { title: 'Gulliver's Travels', author: 'Jonathan Swift' };
const library = new WeakMap();
library.set(book1, true);
library.set(book2, false);
library.set(book3, true);
但是如果你尝试添加对象以外的内容作为键,系统将报错!
library.set('The Grapes of Wrath', false); //Uncaught TypeError: Invalid value used as weak map key(…)
WeakMap
与 Map
在 API
上的区别主要是两个,一是没有遍历操作(即没有key()、values()
和entries()
方法),也没有size
属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。二是无法清空,即不支持clear方法。因此,WeakMap
只有四个方法可用:get()、set()、has()、delete()
。