在ES6中引入JavaScript的新特性中,咱们看到了Set
和Map
的介绍。与常规对象和Array
不一样的是,它们是“键控集合(keyed collections)”。这就是说它们的行为有稍许不一样,而且在特定的上下文中使用,它们能够提供至关大的性能优点。javascript
在这篇文章中,我将剖析Map
,它究竟有何不一样,哪里能够派上用场,相比于常规对象有什么性能优点。java
Map
与常规对象有什么不一样Map
和常规对象主要有2个不一样之处。数组
常规JavaScript对象的键必须是String
或Symbol
,下面的对象说明的这一点:数据结构
const symbol = Symbol();
const string2 = 'string2';
const regularObject = {
string1: 'value1',
[string2]: 'value2',
[symbol]: 'value3'
};
复制代码
相比之下,Map
容许你使用函数、对象和其它简单的类型(包括NaN)做为键,以下代码:函数
const func = () => null;
const object = {};
const array = [];
const bool = false;
const map = new Map();
map.set(func, 'value1');
map.set(object, 'value2');
map.set(array, 'value3');
map.set(bool, 'value4');
map.set(NaN, 'value5');
复制代码
在连接不一样数据类型时,这个特性提供了极大的灵活性。性能
在常规对象中,为了遍历keys、values和entries,你必须将它们转换为数组,如使用Object.keys()
、Object.values()
和Object.entries()
,或者使用for ... in
循环,由于常规对象不能直接遍历,另外for ... in
循环还有一些限制:它仅仅遍历可枚举属性、非Symbol属性,而且遍历的顺序是任意的。测试
而Map
能够直接遍历,而且因为它是键控集合,遍历的顺序和插入键值的顺序是一致的。你可使用for ... of
循环或forEach
方法来遍历Map
的entries,以下代码:ui
for (let [key, value] of map) {
console.log(key);
console.log(value);
};
map.forEach((key, value) => {
console.log(key);
console.log(value);
});
复制代码
还有一个好处就是,你能够调用map.size
属性来获取键值数量,而对于常规对象,为了作到这样你必须先转换为数组,而后获取数组长度,如:Object.keys({}).length
。spa
Map
和Set
有何不一样Map
的行为和Set
很是类似,而且它们都包含一些相同的方法,包括:has、get、set、delete。它们二者都是键控集合,就是说你可使用像forEach
的方法来遍历元素,顺序是按照插入键值排列的。code
最大的不一样是Map
经过键值(key/value)成对出现,就像你能够把一个数组转换为Set
,你也能够把二维数组转换为Map
:
const set = new Set([1, 2, 3, 4]);
const map = new Map([['one', 1], ['two', 2], ['three', 3], ['four', 4]]);
复制代码
要将Map
切换回数组,你可使用ES6的结构语法:
const map = new Map([['one', 1], ['two', 2]]);
const arr = [...map];
复制代码
到目前为止,将Map
与常规对象的互相转换依然不是很方便,因此你可能须要依赖一个函数方法,以下:
const mapToObj = map => {
const obj = {};
map.forEach((key, value) => { obj[key] = value });
return obj;
};
const objToMap = obj => {
const map = new Map();
Object.keys(obj).forEach(key => { map.set(key, obj[key]) });
return map;
};
复制代码
可是如今,在八月份ES2019的首次展现中,咱们看见了Object
引入了2个新方法:Object.entries()
和Object.fromEntries()
,这可使上述方法简化许多:
const obj2 = Object.fromEntries(map);
const map2 = new Map(Object.entries(obj));
复制代码
在你使用Object.fromEntries
转换map为object以前,确保map的key在转换为字符串时会产生惟一的结果,不然你将面临数据丢失的风险。
为了准备测试,我会建立一个对象和一个map,它们都有1000000个相同的键值。
let obj = {}, map = new Map(), n = 1000000;
for (let i = 0; i < n; i++) {
obj[i] = i;
map.set(i, i);
}
复制代码
而后我使用console.time()
来衡量测试,因为我特定的系统和Node.js版本的缘由,时间精度可能会有波动。测试结果展现了使用Map
的性能收益,尤为是添加和删除键值的时。
查询
let result;
console.time('Object');
result = obj.hasOwnProperty('999999');
console.timeEnd('Object');
// Object: 0.250ms
console.time('Map');
result = map.has(999999);
console.timeEnd('Map');
// Map: 0.095ms (2.6 times faster)
复制代码
添加
console.time('Object');
obj[n] = n;
console.timeEnd('Object');
// Object: 0.229ms
console.time('Map');
map.set(n, n);
console.timeEnd('Map');
// Map: 0.005ms (45.8 times faster!)
复制代码
删除
console.time('Object');
delete obj[n];
console.timeEnd('Object');
// Object: 0.376ms
console.time('Map');
map.delete(n);
console.timeEnd('Map');
// Map: 0.012ms (31 times faster!)
复制代码
Map
在什么状况下更慢在测试中,我发现一种状况常规对象的性能更好:使用for
循环去建立常规对象和map。这个结果着实使人震惊,可是没有for
循环,map添加属性的性能赛过常规对象。
console.time('Object');
for (let i = 0; i < n; i++) {
obj[i] = i;
}
console.timeEnd('Object');
// Object: 32.143ms
let obj = {}, map = new Map(), n = 1000000;
console.time('Map');
for (let i = 0; i < n; i++) {
map.set(i, i);
}
console.timeEnd('Map');
// Map: 163.828ms (5 times slower)
复制代码
最后,让咱们看一个Map
比常规对象更合适的例子,好比说咱们想写一个函数去检查2个字符串是否由相同的字符串随机排序。
console.log(isAnagram('anagram', 'gramana')); // Should return true
console.log(isAnagram('anagram', 'margnna')); // Should return false
复制代码
有许多方法能够作到,可是这里,map能够帮忙咱们建立一个最简单、最快速的解决方案:
const isAnagram = (str1, str2) => {
if (str1.length !== str2.length) {
return false;
}
const map = new Map();
for (let char of str1) {
const count = map.has(char) ? map.get(char) + 1 : 1;
map.set(char, count);
}
for (let char of str2) {
if (!map.has(char)) {
return false;
}
const count = map.get(char) - 1;
if (count === 0) {
map.delete(char);
continue;
}
map.set(char, count);
}
return map.size === 0;
};
复制代码
在这个例子中,当涉及到动态添加和删除键值,没法提早确认数据结构(或者说键值的数量)时,map比object更合适。
我但愿这篇文章对你有所帮助,若是你以前没有使用过Map
,不妨开阔你的眼界,衡量现代JavaScript的价值体现。
译者注:我我的不太赞成做者的观点,从以上的描述来看,Map更像是以空间为代价,换取速度上的提高。那么对于空间和速度的衡量,必然存在一个阈值。在数据量比较少时,相比与速度的提高,其牺牲的空间代价更大,此时显然是不适合使用Map;当数据量足够大时,此时空间的代价影响更小。因此,看开发者如何衡量二者之间的关系,选择最优解。