到目前为止,我们已经学习了以下复杂的数据结构
- 对象用于存储键控集合。
- 数组用于存储有序集合。
但这对于实际生活来说还不够。这就是 Map
和 Set
也存在的原因。
Map
Map 是一个键控数据项集合,就像一个 Object
。但主要区别在于 Map
允许任何类型的键。
方法和属性是
new Map()
– 创建映射。map.set(key, value)
– 根据键存储值。map.get(key)
– 通过键返回该值,如果key
不存在于 map 中,则返回undefined
。map.has(key)
– 如果key
存在,则返回true
,否则返回false
。map.delete(key)
– 通过键移除元素(键/值对)。map.clear()
– 从 map 中移除所有内容。map.size
– 返回当前元素数量。
例如
let map = new Map();
map.set('1', 'str1'); // a string key
map.set(1, 'num1'); // a numeric key
map.set(true, 'bool1'); // a boolean key
// remember the regular Object? it would convert keys to string
// Map keeps the type, so these two are different:
alert( map.get(1) ); // 'num1'
alert( map.get('1') ); // 'str1'
alert( map.size ); // 3
正如我们所见,与对象不同,键不会转换为字符串。任何类型的键都是可能的。
map[key]
不是使用 Map
的正确方法虽然 map[key]
也可行,例如我们可以设置 map[key] = 2
,但这将 map
视为一个普通的 JavaScript 对象,因此它暗示了所有相应的限制(仅字符串/符号键等)。
所以我们应该使用 map
方法:set
、get
等。
Map 还可以使用对象作为键。
例如
let john = { name: "John" };
// for every user, let's store their visits count
let visitsCountMap = new Map();
// john is the key for the 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 = {}; // try to use an object
visitsCountObj[ben] = 234; // try to use ben object as the key
visitsCountObj[john] = 123; // try to use john object as the key, ben object will get replaced
// That's what got written!
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
,有 3 种方法
map.keys()
– 返回一个可迭代的键map.values()
– 返回一个可迭代的值map.entries()
– 返回一个可迭代的条目[key, value]
,它在for..of
中默认使用。
例如
let recipeMap = new Map([
['cucumber', 500],
['tomatoes', 350],
['onion', 50]
]);
// iterate over keys (vegetables)
for (let vegetable of recipeMap.keys()) {
alert(vegetable); // cucumber, tomatoes, onion
}
// iterate over values (amounts)
for (let amount of recipeMap.values()) {
alert(amount); // 500, 350, 50
}
// iterate over [key, value] entries
for (let entry of recipeMap) { // the same as of recipeMap.entries()
alert(entry); // cucumber,500 (and so on)
}
迭代的顺序与插入值时的顺序相同。Map
保留此顺序,这与常规 Object
不同。
除此之外,Map
还具有一个内置的 forEach
方法,类似于 Array
// runs the function for each (key, value) pair
recipeMap.forEach( (value, key, map) => {
alert(`${key}: ${value}`); // cucumber: 500 etc
});
Object.entries:从 Object 映射
创建 Map
时,我们可以传递一个包含键/值对的数组(或其他可迭代对象)进行初始化,如下所示
// array of [key, value] pairs
let map = new Map([
['1', 'str1'],
[1, 'num1'],
[true, 'bool1']
]);
alert( map.get('1') ); // str1
如果我们有一个普通对象,并且我们希望从中创建一个 Map
,那么我们可以使用内置方法 Object.entries(obj),它返回一个键/值对数组,该数组的格式完全符合对象。
因此,我们可以像这样从对象创建一个映射
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)
从普通对象创建 Map
。
有一个 Object.fromEntries
方法可以执行相反的操作:给定一个 [key, value]
对数组,它会从中创建一个对象
let prices = Object.fromEntries([
['banana', 1],
['orange', 2],
['meat', 4]
]);
// now prices = { banana: 1, orange: 2, meat: 4 }
alert(prices.orange); // 2
我们可以使用 Object.fromEntries
从 Map
获取一个普通对象。
例如,我们将数据存储在 Map
中,但我们需要将其传递给期望普通对象的第三方代码。
我们开始
let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);
let obj = Object.fromEntries(map.entries()); // make a plain object (*)
// done!
// obj = { banana: 1, orange: 2, meat: 4 }
alert(obj.orange); // 2
调用 map.entries()
返回一个键/值对的可迭代对象,其格式完全符合 Object.fromEntries
。
我们还可以缩短行 (*)
let obj = Object.fromEntries(map); // omit .entries()
这是相同的,因为 Object.fromEntries
期望一个可迭代对象作为参数。不一定是数组。并且 map
的标准迭代返回与 map.entries()
相同的键/值对。因此,我们得到一个与 map
具有相同键/值的普通对象。
Set
Set
是一种特殊的类型集合——“值集合”(没有键),其中每个值只能出现一次。
它的主要方法是
new Set([iterable])
– 创建集合,如果提供了iterable
对象(通常是一个数组),则将值从中复制到集合中。set.add(value)
– 添加一个值,返回集合本身。set.delete(value)
– 删除该值,如果value
在调用时存在,则返回true
,否则返回false
。set.has(value)
– 如果值存在于集合中,则返回true
,否则返回false
。set.clear()
– 从集合中移除所有内容。set.size
– 是元素计数。
主要特点是重复调用 set.add(value)
,其中值相同,不会执行任何操作。这就是每个值仅在 Set
中出现一次的原因。
例如,我们有访客来访,我们希望记住每个人。但重复访问不应导致重复项。访客必须仅被“计数”一次。
Set
正好适合这种情况
let set = new Set();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
// visits, some users come multiple times
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);
// set keeps only unique values
alert( set.size ); // 3
for (let user of set) {
alert(user.name); // John (then Pete and Mary)
}
Set
的替代方案可能是用户数组,以及使用 arr.find 在每次插入时检查重复项的代码。但性能会差很多,因为此方法会遍历整个数组并检查每个元素。Set
在内部针对唯一性检查进行了更好的优化。
遍历 Set
我们可以使用 for..of
或 forEach
循环遍历集合
let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) alert(value);
// the same with forEach:
set.forEach((value, valueAgain, set) => {
alert(value);
});
请注意有趣的事情。在 forEach
中传递的回调函数有 3 个参数:一个 value
,然后是相同的值 valueAgain
,然后是目标对象。事实上,相同的值在参数中出现两次。
这是为了与 Map
兼容,其中传递给 forEach
的回调函数有三个参数。看起来有点奇怪,这是肯定的。但在某些情况下,这可能有助于轻松地用 Set
替换 Map
,反之亦然。
Map
为迭代器提供的相同方法也受支持
set.keys()
– 返回值的迭代对象,set.values()
– 与set.keys()
相同,为了与Map
兼容,set.entries()
– 返回条目[value, value]
的迭代对象,存在是为了与Map
兼容。
总结
Map
– 是键控值集合。
方法和属性
new Map([iterable])
– 创建映射,其中包含用于初始化的[key,value]
对的可选iterable
(例如数组)。map.set(key, value)
– 按键存储值,返回映射本身。map.get(key)
– 通过键返回该值,如果key
不存在于 map 中,则返回undefined
。map.has(key)
– 如果key
存在,则返回true
,否则返回false
。map.delete(key)
– 按键删除元素,如果调用时key
存在,则返回true
,否则返回false
。map.clear()
– 从 map 中移除所有内容。map.size
– 返回当前元素数量。
与普通Object
的区别
- 任何键,对象可以是键。
- 其他便捷方法,
size
属性。
Set
– 是一个唯一值集合。
方法和属性
new Set([iterable])
– 创建集合,带有可选的iterable
(例如数组)值,用于初始化。set.add(value)
– 添加一个值(如果value
存在,则不执行任何操作),返回集合本身。set.delete(value)
– 删除该值,如果value
在调用时存在,则返回true
,否则返回false
。set.has(value)
– 如果值存在于集合中,则返回true
,否则返回false
。set.clear()
– 从集合中移除所有内容。set.size
– 是元素计数。
对Map
和Set
的迭代始终按插入顺序进行,因此我们不能说这些集合是无序的,但我们不能重新排列元素或直接按其编号获取元素。
评论
<code>
标记,对于多行,请将其包装在<pre>
标记中,对于 10 行以上,请使用沙盒 (plnkr、jsbin、codepen…)