学到现在,我们已经了解了以下复杂的数据结构:
- 对象,存储带有键的数据的集合。
- 数组,存储有序集合。
但这还不足以应对现实情况。这就是为什么存在 Map
和 Set
。
Map
Map 是一个带键的数据项的集合,就像一个 Object
一样。 但是它们最大的差别是 Map
允许任何类型的键(key)。
它的方法和属性如下:
new Map()
—— 创建 map。map.set(key, value)
—— 根据键存储值。map.get(key)
—— 根据键来返回值,如果map
中不存在对应的key
,则返回undefined
。map.has(key)
—— 如果key
存在则返回true
,否则返回false
。map.delete(key)
—— 删除指定键的值。map.clear()
—— 清空 map。map.size
—— 返回当前元素个数。
举个例子:
let map = new Map();
map.set('1', 'str1'); // 字符串键
map.set(1, 'num1'); // 数字键
map.set(true, 'bool1'); // 布尔值键
// 还记得普通的 Object 吗? 它会将键转化为字符串
// Map 则会保留键的类型,所以下面这两个结果不同:
alert( map.get(1) ); // 'num1'
alert( map.get('1') ); // 'str1'
alert( map.size ); // 3
如我们所见,与对象不同,键不会被转换成字符串。键可以是任何类型。
map[key]
不是使用 Map
的正确方式虽然 map[key]
也有效,例如我们可以设置 map[key] = 2
,这样会将 map
视为 JavaScript 的 plain object,因此它暗含了所有相应的限制(仅支持 string/symbol 键等)。
所以我们应该使用 map
方法:set
和 get
等。
Map 还可以使用对象作为键。
例如:
let john = { name: "John" };
// 存储每个用户的来访次数
let visitsCountMap = new Map();
// john 是 Map 中的键
visitsCountMap.set(john, 123);
alert( visitsCountMap.get(john) ); // 123
使用对象作为键是 Map
最值得注意和重要的功能之一。在 Object
中,我们则无法使用对象作为键。在 Object
中使用字符串作为键是可以的,但我们无法使用另一个 Object
作为 Object
中的键。
我们来尝试一下:
let john = { name: "John" };
let ben = { name: "Ben" };
let visitsCountObj = {}; // 尝试使用对象
visitsCountObj[ben] = 234; // 尝试将对象 ben 用作键
visitsCountObj[john] = 123; // 尝试将对象 john 用作键,但我们会发现使用对象 ben 作为键存下的值会被替换掉
// 变成这样了!
alert( visitsCountObj["[object Object]"] ); // 123
因为 visitsCountObj
是一个对象,它会将所有 Object
键例如上面的 john
和 ben
转换为字符串 "[object Object]"
。这显然不是我们想要的结果。
Map
是怎么比较键的?Map
使用 SameValueZero 算法来比较键是否相等。它和严格等于 ===
差不多,但区别是 NaN
被看成是等于 NaN
。所以 NaN
也可以被用作键。
这个算法不能被改变或者自定义。
每一次 map.set
调用都会返回 map 本身,所以我们可以进行“链式”调用:
map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
Map 迭代
如果要在 map
里使用循环,可以使用以下三个方法:
map.keys()
—— 遍历并返回一个包含所有键的可迭代对象,map.values()
—— 遍历并返回一个包含所有值的可迭代对象,map.entries()
—— 遍历并返回一个包含所有实体[key, value]
的可迭代对象,for..of
在默认情况下使用的就是这个。
例如:
let recipeMap = new Map([
['cucumber', 500],
['tomatoes', 350],
['onion', 50]
]);
// 遍历所有的键(vegetables)
for (let vegetable of recipeMap.keys()) {
alert(vegetable); // cucumber, tomatoes, onion
}
// 遍历所有的值(amounts)
for (let amount of recipeMap.values()) {
alert(amount); // 500, 350, 50
}
// 遍历所有的实体 [key, value]
for (let entry of recipeMap) { // 与 recipeMap.entries() 相同
alert(entry); // cucumber,500 (and so on)
}
迭代的顺序与插入值的顺序相同。与普通的 Object
不同,Map
保留了此顺序。
除此之外,Map
有内建的 forEach
方法,与 Array
类似:
// 对每个键值对 (key, value) 运行 forEach 函数
recipeMap.forEach( (value, key, map) => {
alert(`${key}: ${value}`); // cucumber: 500 etc
});
Object.entries:从对象创建 Map
当创建一个 Map
后,我们可以传入一个带有键值对的数组(或其它可迭代对象)来进行初始化,如下所示:
// 键值对 [key, value] 数组
let map = new Map([
['1', 'str1'],
[1, 'num1'],
[true, 'bool1']
]);
alert( map.get('1') ); // str1
如果我们想从一个已有的普通对象(plain object)来创建一个 Map
,那么我们可以使用内建方法 Object.entries(obj),该方法返回对象的键/值对数组,该数组格式完全按照 Map
所需的格式。
所以可以像下面这样从一个对象创建一个 Map:
let obj = {
name: "John",
age: 30
};
let map = new Map(Object.entries(obj));
alert( map.get('name') ); // John
这里,Object.entries
返回键/值对数组:[ ["name","John"], ["age", 30] ]
。这就是 Map
所需要的格式。
Object.fromEntries:从 Map 创建对象
我们刚刚已经学习了如何使用 Object.entries(obj)
从普通对象(plain object)创建 Map
。
Object.fromEntries
方法的作用是相反的:给定一个具有 [key, value]
键值对的数组,它会根据给定数组创建一个对象:
let prices = Object.fromEntries([
['banana', 1],
['orange', 2],
['meat', 4]
]);
// 现在 prices = { banana: 1, orange: 2, meat: 4 }
alert(prices.orange); // 2
我们可以使用 Object.fromEntries
从 Map
得到一个普通对象(plain object)。
例如,我们在 Map
中存储了一些数据,但是我们需要把这些数据传给需要普通对象(plain object)的第三方代码。
我们来开始:
let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);
let obj = Object.fromEntries(map.entries()); // 创建一个普通对象(plain object)(*)
// 完成了!
// obj = { banana: 1, orange: 2, meat: 4 }
alert(obj.orange); // 2
调用 map.entries()
将返回一个可迭代的键/值对,这刚好是 Object.fromEntries
所需要的格式。
我们可以把带 (*)
这一行写得更短:
let obj = Object.fromEntries(map); // 省掉 .entries()
上面的代码作用也是一样的,因为 Object.fromEntries
期望得到一个可迭代对象作为参数,而不一定是数组。并且 map
的标准迭代会返回跟 map.entries()
一样的键/值对。因此,我们可以获得一个普通对象(plain object),其键/值对与 map
相同。
Set
Set
是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次。
它的主要方法如下:
new Set(iterable)
—— 创建一个set
,如果提供了一个iterable
对象(通常是数组),将会从数组里面复制值到set
中。set.add(value)
—— 添加一个值,返回 set 本身set.delete(value)
—— 删除值,如果value
在这个方法调用的时候存在则返回true
,否则返回false
。set.has(value)
—— 如果value
在 set 中,返回true
,否则返回false
。set.clear()
—— 清空 set。set.size
—— 返回元素个数。
它的主要特点是,重复使用同一个值调用 set.add(value)
并不会发生什么改变。这就是 Set
里面的每一个值只出现一次的原因。
例如,我们有客人来访,我们想记住他们每一个人。但是已经来访过的客人再次来访,不应造成重复记录。每个访客必须只被“计数”一次。
Set
可以帮助我们解决这个问题:
let set = new Set();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
// visits,一些访客来访好几次
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);
// set 只保留不重复的值
alert( set.size ); // 3
for (let user of set) {
alert(user.name); // John(然后 Pete 和 Mary)
}
Set
的替代方法可以是一个用户数组,用 arr.find 在每次插入值时检查是否重复。但是这样性能会很差,因为这个方法会遍历整个数组来检查每个元素。Set
内部对唯一性检查进行了更好的优化。
Set 迭代(iteration)
我们可以使用 for..of
或 forEach
来遍历 Set:
let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) alert(value);
// 与 forEach 相同:
set.forEach((value, valueAgain, set) => {
alert(value);
});
注意一件有趣的事儿。forEach
的回调函数有三个参数:一个 value
,然后是 同一个值 valueAgain
,最后是目标对象。没错,同一个值在参数里出现了两次。
forEach
的回调函数有三个参数,是为了与 Map
兼容。当然,这看起来确实有些奇怪。但是这对在特定情况下轻松地用 Set
代替 Map
很有帮助,反之亦然。
Map
中用于迭代的方法在 Set
中也同样支持:
set.keys()
—— 遍历并返回一个包含所有值的可迭代对象,set.values()
—— 与set.keys()
作用相同,这是为了兼容Map
,set.entries()
—— 遍历并返回一个包含所有的实体[value, value]
的可迭代对象,它的存在也是为了兼容Map
。
总结
Map
—— 是一个带键的数据项的集合。
方法和属性如下:
new Map([iterable])
—— 创建 map,可选择带有[key,value]
对的iterable
(例如数组)来进行初始化。map.set(key, value)
—— 根据键存储值,返回 map 自身。map.get(key)
—— 根据键来返回值,如果map
中不存在对应的key
,则返回undefined
。map.has(key)
—— 如果key
存在则返回true
,否则返回false
。map.delete(key)
—— 删除指定键对应的值,如果在调用时key
存在,则返回true
,否则返回false
。map.clear()
—— 清空 map 。map.size
—— 返回当前元素个数。
与普通对象 Object
的不同点:
- 任何键、对象都可以作为键。
- 有其他的便捷方法,如
size
属性。
Set
—— 是一组唯一值的集合。
方法和属性:
new Set([iterable])
—— 创建 set,可选择带有iterable
(例如数组)来进行初始化。set.add(value)
—— 添加一个值(如果value
存在则不做任何修改),返回 set 本身。set.delete(value)
—— 删除值,如果value
在这个方法调用的时候存在则返回true
,否则返回false
。set.has(value)
—— 如果value
在 set 中,返回true
,否则返回false
。set.clear()
—— 清空 set。set.size
—— 元素的个数。
在 Map
和 Set
中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。