利用JavaScript的Map提升性能的方法详解
前言
在ES6中引入JavaScript的新特性中,我们看到了Set和Map的介绍。与常规对象和Array不同的是,它们是“键控集合(keyedcollections)”。这就是说它们的行为有稍许不同,并且在特定的上下文中使用,它们可以提供相当大的性能优势。
在这篇文章中,我将剖析Map,它究竟有何不同,哪里可以派上用场,相比于常规对象有什么性能优势。
Map与常规对象有什么不同
Map和常规对象主要有2个不同之处。
1.无限制的键(Key)
常规JavaScript对象的键必须是String或Symbol,下面的对象说明的这一点:
constsymbol=Symbol();
conststring2='string2';
constregularObject={
string1:'value1',
[string2]:'value2',
[symbol]:'value3'
};
相比之下,Map允许你使用函数、对象和其它简单的类型(包括NaN)作为键,如下代码:
constfunc=()=>null;
constobject={};
constarray=[];
constbool=false;
constmap=newMap();
map.set(func,'value1');
map.set(object,'value2');
map.set(array,'value3');
map.set(bool,'value4');
map.set(NaN,'value5');
在链接不同数据类型时,这个特性提供了极大的灵活性。
2.直接遍历
在常规对象中,为了遍历keys、values和entries,你必须将它们转换为数组,如使用Object.keys()、Object.values()和Object.entries(),或者使用for...in循环,因为常规对象不能直接遍历,另外for...in循环还有一些限制:它仅仅遍历可枚举属性、非Symbol属性,并且遍历的顺序是任意的。
而Map可以直接遍历,并且由于它是键控集合,遍历的顺序和插入键值的顺序是一致的。你可以使用for...of循环或forEach方法来遍历Map的entries,如下代码:
for(let[key,value]ofmap){
console.log(key);
console.log(value);
};
map.forEach((key,value)=>{
console.log(key);
console.log(value);
});
还有一个好处就是,你可以调用map.size属性来获取键值数量,而对于常规对象,为了做到这样你必须先转换为数组,然后获取数组长度,如:Object.keys({}).length。
Map和Set有何不同
Map的行为和Set非常相似,并且它们都包含一些相同的方法,包括:has、get、set、delete。它们两者都是键控集合,就是说你可以使用像forEach的方法来遍历元素,顺序是按照插入键值排列的。
最大的不同是Map通过键值(key/value)成对出现,就像你可以把一个数组转换为Set,你也可以把二维数组转换为Map:
constset=newSet([1,2,3,4]); constmap=newMap([['one',1],['two',2],['three',3],['four',4]]);
类型转换
要将Map切换回数组,你可以使用ES6的结构语法:
constmap=newMap([['one',1],['two',2]]); constarr=[...map];
到目前为止,将Map与常规对象的互相转换依然不是很方便,所以你可能需要依赖一个函数方法,如下:
constmapToObj=map=>{
constobj={};
map.forEach((key,value)=>{obj[key]=value});
returnobj;
};
constobjToMap=obj=>{
constmap=newMap();
Object.keys(obj).forEach(key=>{map.set(key,obj[key])});
returnmap;
};
但是现在,在八月份ES2019的首次展示中,我们看见了Object引入了2个新方法:Object.entries()和Object.fromEntries(),这可以使上述方法简化许多:
constobj2=Object.fromEntries(map); constmap2=newMap(Object.entries(obj));
在你使用Object.fromEntries转换map为object之前,确保map的key在转换为字符串时会产生唯一的结果,否则你将面临数据丢失的风险。
性能测试
为了准备测试,我会创建一个对象和一个map,它们都有1000000个相同的键值。
letobj={},map=newMap(),n=1000000;
for(leti=0;i
然后我使用console.time()来衡量测试,由于我特定的系统和Node.js版本的原因,时间精度可能会有波动。测试结果展示了使用Map的性能收益,尤其是添加和删除键值的时。
查询
letresult;
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.6timesfaster)
添加
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.8timesfaster!)
删除
console.time('Object');
deleteobj[n];
console.timeEnd('Object');
//Object:0.376ms
console.time('Map');
map.delete(n);
console.timeEnd('Map');
//Map:0.012ms(31timesfaster!)
Map在什么情况下更慢
在测试中,我发现一种情况常规对象的性能更好:使用for循环去创建常规对象和map。这个结果着实令人震惊,但是没有for循环,map添加属性的性能胜过常规对象。
console.time('Object');
for(leti=0;i
举个例子
最后,让我们看一个Map比常规对象更合适的例子,比如说我们想写一个函数去检查2个字符串是否由相同的字符串随机排序。
console.log(isAnagram('anagram','gramana'));//Shouldreturntrue
console.log(isAnagram('anagram','margnna'));//Shouldreturnfalse
有许多方法可以做到,但是这里,map可以帮忙我们创建一个最简单、最快速的解决方案:
constisAnagram=(str1,str2)=>{
if(str1.length!==str2.length){
returnfalse;
}
constmap=newMap();
for(letcharofstr1){
constcount=map.has(char)?map.get(char)+1:1;
map.set(char,count);
}
for(letcharofstr2){
if(!map.has(char)){
returnfalse;
}
constcount=map.get(char)-1;
if(count===0){
map.delete(char);
continue;
}
map.set(char,count);
}
returnmap.size===0;
};
在这个例子中,当涉及到动态添加和删除键值,无法提前确认数据结构(或者说键值的数量)时,map比object更合适。
我希望这篇文章对你有所帮助,如果你之前没有使用过Map,不妨开阔你的眼界,衡量现代JavaScript的价值体现。
译者注:我个人不太同意作者的观点,从以上的描述来看,Map更像是以空间为代价,换取速度上的提升。那么对于空间和速度的衡量,必然存在一个阈值。在数据量比较少时,相比与速度的提升,其牺牲的空间代价更大,此时显然是不适合使用Map;当数据量足够大时,此时空间的代价影响更小。所以,看开发者如何衡量两者之间的关系,选择最优解。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。