JS 中 Map 对象高级用法
继续讨论 Map
对象,重点介绍它的遍历方法、高级用法,及其与 Object
的区别。
对了,在 Map
和 Object
之间,你更倾向于使用哪一个呢?在项目中,它们是否曾对你造成过困扰呢?如果你没有肯定的答案,希望在接下来的文章中能帮你找到。
现在,让我们开始吧~
1. 如何遍历 Map
遍历 Map
对象是一个非常常见的操作,JS 为我们提供了哪些方法呢?
1.1 keys()
方法
该方法返回一个新的迭代器对象,它包括 Map
对象中每个元素的键。
const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);
for (const key of map.keys()) {
console.log(key);
}
// 输出:'name' 'age'
1.2 values()
方法
该方法返回一个新的迭代器对象,它包括 Map
对象中每个元素的值。
const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);
for (const value of map.values()) {
console.log(value);
}
// 输出:'Alice' 25
1.3 entries()
方法
该方法返回一个新的迭代器对象,它包括 Map
对象中每个元素的键值对。
const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);
for (const [key, value] of map.entries()) {
console.log(key + ': ' + value);
}
// 输出:'name: Alice' 'age: 25'
1.4 forEach()
方法
此方法接受一个回调函数作为参数,Map
对象中的每个元素都会调用一次这个回调函数。回调函数中的参数依次为:value
、key
、mapObject
。
const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);
map.forEach((value, key) => {
console.log(key, value);
});
// 输出:'name' 'Alice' 'age' 25
上面提到的这四个方法都可以用来遍历 Map
对象,你可以根据实际需求选择合适的方法。
2. Map
对象的进阶用法
了解了如何遍历 Map
对象后,我们来看一下 Map
都有哪些高级用法:
2.1 Map
和 Array
的相互转化
有时我们需要在 Map
和 Array
之间相互转化,比如将数组转换为字典,或者从字典转换为数组时,我们就可以结合 Array
构造函数和扩展运算符来实现。
let kvArray = [['key1', 'value1'], ['key2', 'value2']];
let myMap = new Map(kvArray);
console.log(myMap); // Map(2) {"key1" => "value1", "key2" => "value2"}
let arrayFromMap = Array.from(myMap);
console.log(arrayFromMap); // [["key1", "value1"], ["key2", "value2"]]
2.2 Map
的合并和复制
当需要将多个映射结构合并为一个,或者在需要复制 Map
对象时,可以用 Map
对象的 set()
方法和扩展运算符来实现。比如,你创建了一个 Map
对象,然后通过 forEach()
方法和 set()
方法将一个 Map
对象的所有键值对都复制到一个新的 Map
对象中。当然,你也可以用扩展运算符将多个 Map
对象合并为一个新的 Map
对象。
let map1 = new Map().set('a', 1).set('b', 2);
let map2 = new Map().set('c', 3).set('d', 4);
// 合并
let merged = new Map([...map1, ...map2]);
console.log(merged); // Map(4) {"a" => 1, "b" => 2, "c" => 3, "d" => 4}
// 复制
let copied = new Map(merged);
console.log(copied); // Map(4) {"a" => 1, "b" => 2, "c" => 3, "d" => 4}
不过,需要注意的是,通过扩展运算符和 new Map
复制 Map
对象的方式是浅拷贝。也就是说,如果 Map
对象的键或值是一个对象或者数组,那么复制后的 Map
对象中这个键或值将与原来 Map
中的相同,他们引用的是同一个对象或数组。这时,如果你修改了复制后的 Map
对象中的这个对象或数组,原来 Map
对象中相同对象或数组也会被修改。
let originalMap = new Map();
let objKey = {id: 1};
let objValue = {name: 'value'};
originalMap.set(objKey, objValue);
let copiedMap = new Map(originalMap);
console.log(copiedMap.get(objKey) === objValue); // true
// 修改复制的 Map 中的对象
copiedMap.get(objKey).name = 'new value';
console.log(originalMap.get(objKey).name); // "new value"
假如你要实现深拷贝,可以结合 JSON
方法来实现。但是要注意,使用 JSON
方法只能用于键和值都可以被 JSON
序列化的情况。
let originalMap = new Map();
let objKey = {id: 1};
let objValue = {name: 'value'};
originalMap.set(JSON.stringify(objKey), objValue);
let deepCopiedMap = new Map();
for (let [key, value] of originalMap) {
deepCopiedMap.set(JSON.parse(key), JSON.parse(JSON.stringify(value)));
}
console.log(deepCopiedMap.get(JSON.parse(JSON.stringify(objKey))) === objValue); // false
// 修改深拷贝的 Map 中的对象
deepCopiedMap.get(JSON.parse(JSON.stringify(objKey))).name = 'new value';
console.log(originalMap.get(JSON.stringify(objKey)).name); // "value"
因 JSON
方法对 Map
的键和值有较多的限制,若要实现更为通用的 Map
的深拷贝,你可能要自己实现深拷贝函数,或者使用第三方库也行,比如 lodash
的 _.cloneDeep()
方法。
2.3 Map
的大小
要获取 Map
的大小,可以直接通过 Map
上的 size
属性来实现。相对于 Object
,这是 Map
对象的一大优势。要知道,在 Object
中你需要手动计算属性的数量。
let map = new Map().set('a', 1).set('b', 2);
console.log(map.size); // 2
2.4 有序性
Map
对象会保留键值对的插入顺序,这是 Object
所不具备的。比如,需要对键值对进行排序或迭代的场景,Map
的这个特性就比 Object
更为有用。
let map = new Map();
map.set('a', 1);
map.set('b', 2);
map.set('c', 3);
for (let key of map.keys()) {
console.log(key);
}
// 'a'
// 'b'
// 'c'
2.5 弱映射
JS 提供了一种特殊类型的 Map
——WeakMap
,其键必须是对象,且对键的引用是弱引用。WeakMap
特别适用于保存对其键的临时引用,这种弱引用不会阻止 JS 的垃圾回收器的自动清理。
let weakmap = new WeakMap();
let obj = {};
weakmap.set(obj, 'hello');
console.log(weakmap.get(obj)); // "hello"
obj = null;
// obj 被清理, weakmap 现在不包含任何元素
3. Map
与 Object
的比较
了解了 Map
对象的高级用法后,我们来看看 Map
对象与 Object
对象之间的主要区别:
3.1 键的类型
我们知道,JS 对象中的键只能是字符串或符号,而 Map
的键可以是任何类型。
// 使用 Object
let obj = {};
obj[5] = 'foo';
console.log(obj['5']); // 'foo', 因为键 '5' 和 '5' 在 Object 中被认为是相同的。
// 使用 Map
let map = new Map();
map.set(5, 'foo');
console.log(map.get(5)); // 'foo'
console.log(map.get('5')); // undefined, 因为 5 和 '5' 在 Map 中被认为是不同的键。
在这个示例中,我们分别演示了将指定的键添加到 Object
和 Map
中。
通过观察可以发现,在 Object
中,所有非符号键都会被转换为字符串。所以呢,如果你这时候通过数字键检索值时,实际上使用的是字符串来检索值的。
再者,Map
允许你使用任何类型的键,这就意味着 5
和 '5'
被视为不同的键。
在实际应用中,这种特性使得 Map
可以存储更加复杂和多样化的数据结构。比如,你可以使用对象、数组或甚至函数作为 Map
的键,而这在 Object
中却是无法实现的
3.2 键的顺序
Map
对象会保持键值对的插入顺序,而 Object
对象则不会。
// 使用 Object
let obj = {
'b': 'foo',
'a': 'bar'
};
console.log(Object.keys(obj)); // ['b', 'a'], Object 不保证键的顺序。
// 使用 Map
let map = new Map();
map.set('b', 'foo');
map.set('a', 'bar');
console.log([...map.keys()]); // ['b', 'a'], Map 保持键的插入顺序。
在向 Object
中添加键值对时,Object
不能保证返回的键的顺序与插入时保持一致,但是 Map
一定会按照插入时的顺序原封不动的返回。
一个典型的场景,假如要实现一个有序的字典或列表时,Map
的这种有序性将会非常有用。
3.3 性能
在频繁添加和删除键值对的操作中,Map
的性能通常优于 Object
。
// 使用 Object
let obj = { 'a': 'foo', 'b': 'bar', 'c': 'baz' };
delete obj.b;
console.log(obj); // { 'a': 'foo', 'c': 'baz' }, 删除操作可能会影响 Object 的性能。
// 使用 Map
let map = new Map();
map.set('a', 'foo').set('b', 'bar').set('c', 'baz');
map.delete('b');
console.log([...map]); // [['a', 'foo'], ['c', 'baz']], Map 对象在频繁添加和删除键值对的操作中性能更优。
其实,在数据量较小或者没有频繁添加、删除键值对时,Object
和 Map
的性能差距可以忽略不计。
但是,如果要操作的数据量非常大,或者很频繁的添加、删除键值对,这时候还是要考虑使用 Map
的。
比如,在缓存或者其他一些动态数据集合中,Map
要比 Object
更有优势。
4. Map
对象实例分析
现在你应该对 Map
对象有了比较深入的理解,下面我们再通过两个示例来展示 Map
对象在实际问题中的应用。
4.1 使用 Map
实现 LRU Cache(最近最少使用页面替换算法)
LRU Cache 是一种常见的缓存策略,它可以把最近最少使用的数据给淘汰掉。
下面是一个简单的示例:
class LRUCache {
constructor(maxSize) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(key) {
if(!this.cache.has(key)) return -1;
const temp = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, temp);
return temp;
}
put(key, value) {
if(this.cache.has(key)) this.cache.delete(key);
else if(this.cache.size >= this.maxSize) this.cache.delete(this.cache.keys().next().value);
this.cache.set(key, value);
}
}
在 Map
对象中获取缓存数据时,会将数据从 Map
中移出出去,然后再将移出的数据重新放回到 Map
的末尾,这能保证最近使用过的数据总是在 Map
对象的结尾(因 Map
对象的有序性)。
当缓存已满时,就将 Map
开头的数据(也就是最近最少使用的数据)删除,然后再插入新的数据。
4.2 使用 Map 计算字符串中每个字符的出现次数
不知道你有没有遇到过类似的问题,要求统计一个字符串中每个字符出现的次数。
针对这个问题,用 Map
即可解决。
function countChars(str) {
const countMap = new Map();
for (let char of str) {
if (countMap.has(char)) {
countMap.set(char, countMap.get(char) + 1);
} else {
countMap.set(char, 1);
}
}
return countMap;
}
console.log(countChars("hello world"));
// Map(8) { 'h' => 1, 'e' => 1, 'l' => 3, 'o' => 2, ' ' => 1, 'w' => 1, 'r' => 1, 'd' => 1 }
上面的代码遍历了输入的字符串,对于其中的每个字符,如果它已经存在于 Map
中,那么就将其数量 +1;如果它还不在 Map
中,那就将其添加到 Map
中,并将其数量设置为 1。
最后的 Map
中,Map
对象的每个键就是字符串中的字符,值就是该字符出现的次数。
5. Map
对象的坑和注意事项
这里要提一下,当我们使用新的工具或技术时,了解其优点和功能固然重要,但是,同样重要的是理解其局限性。稍不注意,你可能就掉坑里了~
对于 JS 中的 Map
对象来说,虽然它提供了不少很强大的功能,但在某些情况下,使用 Map
对象或许不是最好的选择。
另外,Map
对象在处理某些特殊的值的时候,其行为也可能与我们的预期不同。
5.1 什么情况下不应该使用 Map 对象?
首先,数据量小且固定。如果你存储的数据量很小,并且键名是固定的,那么使用普通的 Object
对象会更为简单和高效。因为 Object
的创建和访问速度通常比 Map
要快不少。
再一个,不需要顺序的键。你已经知道,Map
对象会保持键值对的插入顺序,这在某些场景下非常有用。而如果你没有这方面的需求的话,使用 Object
对象反而会更好。
最后是不需要操作复杂数据的情况。Map
对象提供的方法都比较简单,如果你有需要操作复杂数据的需求,比如过滤、映射、排序等,这时候使用 Array
或其他数据结构或许会更方便。
5.2 Map 对象在特殊情况下的行为
在使用 Map
时,还要注意一些特殊情况,下面提到了几种容易让人造成困扰的地方。
NaN 和 NaN:在 Map
对象中,NaN
被视为与自身相等的唯一值。也就是说,你可以将 NaN
作为对象中的一个键,而且 Map
对象会将所有 NaN
视为同一个键。
let map = new Map();
map.set(NaN, 'value');
console.log(map.get(NaN)); // 'value'
+0 和 -0:+0
和 -0
在 Map
对象中被视为同一个键。而在 Object
对象中则不同,在 Object
对象中,+0
和 -0
作为键是不同的。
let map = new Map();
map.set(+0, 'positive');
map.set(-0, 'negative');
console.log(map.get(+0)); // 'negative'
console.log(map.get(-0)); // 'negative'
键的比较:在 JS 中,Map
对象在比较键时使用的是一种叫做”same-value equality”的算法。这个算法的主要特点是:
- 它会认为
NaN
等于NaN
; +0
和-0
被认为相同。
这与 JS 中的严格相等运算符(===
)的行为不同,在严格相等运算符中,NaN
不等于 NaN
,+0
和 -0
也是不同的。
let map = new Map();
// 在 Map 对象中,NaN 被视为与自身相等的唯一值
map.set(NaN, 'test');
console.log(map.get(NaN)); // 输出 'test'
// 在 Map 对象中,+0 和 -0 被视为同一个键
map.set(+0, 'positive');
map.set(-0, 'negative');
console.log(map.get(+0)); // 输出 'negative'
console.log(map.get(-0)); // 输出 'negative'
在上面的例子中你可以看到,虽然在 JS中,NaN
不等于 NaN
,+0
和 -0
不相同,但是在 Map
对象中,NaN
被视为与自身相等的唯一值,而 +0
和 -0
被视为同一个键。在使用 Map
对象时,这一点是需要格外注意的。
6. 小结
我们已经深入探讨了 JavaScript 中的 Map
对象,包括其遍历方法、进阶用法,以及与 Object
的比较。我们还通过实例分析,展示了 Map
在实际问题中的应用。希望你能从中收获一些有用的知识,也希望这些知识能帮助你在编程中更好地使用 Map
对象。