当前位置: 首页 > 科技观察

我应该使用对象还是地图?

时间:2023-03-17 12:26:42 科技观察

前言在日常的JavaScript项目中,最常用的数据结构就是各种形式的键值对。在JavaScript中,除了最基本的Object格式外,ES6新增的Map也是一种键值对格式。在许多情况下,它们的用法非常接近。我想知道是否有人像我一样纠结于选择使用哪一个?在本才最近的项目中,又遇到了这样的麻烦,于是索性不停的做一件事和两件事,比较用哪个。本文将探究Object和Map的区别,从多个角度比较Object和Map:用法上的区别:在某些情况下,用法上会完全不同修改Performancedifference:Speedandmemoryusage希望看完这篇文章后,你可以在以后的项目中做出更合适的选择。用法对比对于Object,其键(key)的类型只能是string、number或Symbol;对于Map,它可以是任何类型。(包括Date、Map或自定义对象)Map中的元素会保持插入时的顺序;而Object在插入时不会完全保持顺序,而是会按照如下规则排序:非负整数会最先列出,排序是从小到大的数字顺序依次是所有的字符串、负整数、和将列出浮点数。该顺序基于插入的顺序。符号将在最后列出。符号也根据插入顺序排序。读取Map的长度很简单,只需要调用它的.size()方法即可;而读取Object的长度则需要额外计算:Object.keys(obj).lengthMap是一个可迭代对象,所以里面的键值对是可以通过forof循环或者.foreach()方法来迭代的;而普通的对象键值对默认是不可迭代的,只能通过forin循环访问(或者使用Object.keys(o),Object.values(o),Object.entries(o)获取代表的数字)键或值)迭代的顺序是上面提到的顺序。consto={};constm=newMap();o[Symbol.iterator]!==undefined;//falsem[Symbol.iterator]!==undefined;//true当Map中加入新的key时,会不被覆盖原型上的键;当在Object中添加一个新的键时,它可能会覆盖其原型上的键:Object.prototype.x=1;consto={x:2};constm=newMap([[x,2]]);o.x;//2、x=1被覆盖m.x;//1、x=1不会被覆盖JSON默认支持Object,不支持Map。如果想通过JSON传输Map,需要使用.toJSON()方法,然后在JSON.parse()中传入恢复函数进行恢复。对于JSON,这里就不细说了。有兴趣的朋友可以看看这个:JSON序列化与解析consto={x:1};constm=newMap([['x',1]]);consto2=JSON.parse(JSON.stringify(o));//{x:1}constm2=JSON.parse(JSON.stringify(m))//{}语法比较和创建的区别Objectconsto={};//对象字面量consto=newObject();//调用theconstructorconsto=Object.create(null);//调用静态方法Object.create对于Object,我们在95%+的情况下都会选择对象字面量,它不仅是最容易写的,而且会更在性能方面比下面的函数调用更高效。对于构造函数,唯一可能的用例是显式封装原始类型;而Object.create可以为对象设置原型。Mapconstm=newMap();//调用构造函数不同于Object,Map没有那么多花哨的创建方法,通常只使用它的构造函数来创建。除了上述方法外,我们还可以使用Function.prototype.apply()、Function.prototype.call()、reflect.apply()、Reflect.construct()方法调用Object和Map或Object的构造函数.create()方法,这里就不展开了。添加/读取/删除元素时的区别Objectconsto={};//添加/修改o.x=1;o['y']=2;//读取o.x;//1o['y'];//2//或者使用ES2020新的条件属性访问表达式读取o?.x;//1o?.['y'];//2//删除deleteo.b;对于新元素,似乎使用第一种方法更简单,但也有一些限制:属性名不能包含空格和标点符号属性名不能以数字开头有关条件属性访问表达式的更多信息,您可以看到:条件属性访问表达式公式Mapconstm=newMap();//添加/修改m.set('x',1);//读取map.get('x');//删除map.delete('b');对于简单的增删改查,Map上的方法使用起来也很方便;但是,在进行联动操作时,Map中的用法会稍微臃肿:constm=newMap([['x',1]]);//如果我们想对x的值加1,就需要这样做:m.set('x',m.get('x')+1);m.get('x');//2consto={x:1};//这样修改起来会方便很多theobject:o.x++;o.x//2性能比较下面我们来讨论一下Object和Map的性能。不知道大家有没有听说过Map的性能比Object好。反正我已经看过很多次了,甚至在JSElevation4中提到了Map相对于Object的性能优势;但是泛化性能很好。所以我打算做一些测试来比较差异。测试方法我这里进行的性能测试都是基于v8引擎进行的。速度会通过JS标准库自带的performance.now()函数判断,内存占用会通过Chromedevtool中的内存查看。对于速度测试,很多时候performance.now()会返回0,因为单个操作太快了。于是循环了10000次,判断了时间差。因为循环本身也会占用一部分时间,下面的测试只能作为一个粗略的参考。创建时用于性能测试的代码如下:letn,n2=5;//speedwhile(n2--){letp1=performance.now();n=10000;while(n--){leto={};}letp2=performance.now();n=10000;while(n--){letm=newMap();}letp3=performance.now();console.log(`Object:${(p2-p1).toFixed(3)}ms,Map:${(p3-p2).toFixed(3)}ms`);}//内存类Test{}lettest=newTest();test.o=o;test.m=m;首先要比较的是创建Object和Map时的性能。创建速度如下:我们可以发现Object的创建速度会比Map快。内存使用情况如下:我们主要关注它的RetainedSize,代表为其分配的空间。(也就是删除时释放的内存大小)通过对比我们可以发现,一个空的Object会比一个空的Map占用更少的内存。所以这一轮Object获胜。添加元素时进行性能测试的代码如下:console.clear();letn,n2=5;leto={},m=newMap();//speedwhile(n2--){letp1=performance.now();n=10000;while(n--){o[Math.random()]=Math.random();}letp2=performance.now();n=10000;while(n--){m.set(Math.random(),Math.random());}letp3=performance.now();console.log(`Object:${(p2-p1).toFixed(3)}ms,Map:${(p3-p2).toFixed(3)}ms`);}//内存类Test{}lettest=newTest();test.o=o;test.m=m;新建元素的速度如下:我们可以发现,在新建元素的时候,Map会比Object快。内存占用情况如下:通过对比我们可以发现,当元素数量达到一定数量时,Object会比Map多占用78%左右的内存。我也进行了很多测试,发现在元素足够多的情况下,这个百分比非常稳定。因此,当需要进行很多新操作,需要存储大量数据时,使用Map效率更高。读取元素时进行性能测试的代码如下:letn;leto={},m=newMap();n=10000;while(n--){o[Math.random()]=Math.random();}n=10000;while(n--){m.set(Math.random(),Math.random());}letp1=performance.now();for(keyino){letk=o[key];}letp2=performance.now();for([key]ofm){letk=m.get(key);}letp3=performance.now();`Object:${(p2-p1).toFixed(3)}ms,Map:${(p3-p2).toFixed(3)}ms`显示读取元素的速度如下:通过对比我们可以发现Object略有优势,但总体差别不大.删除元素时的性能不知道大家有没有听说过delete操作符性能低下,甚至很多时候为了性能宁愿把值设置为undefined也不愿意使用delete操作符。但实际上在v8最近的优化下,它的效率已经提升了很多。测试使用的代码如下:letn;leto={},m=newMap();n=10000;while(n--){o[Math.random()]=Math.random();}n=10000;while(n--){m.set(Math.random(),Math.random());}letp1=performance.now();for(keyino){deleteo[key];}letp2=performance.now();for([key]ofm){m.delete(key);}letp3=performance.now();`Object:${(p2-p1).toFixed(3)}ms,Map:${(p3-p2).toFixed(3)}ms`对于删除元素的速度表现如下:我们可以发现Map在删除元素的时候速度稍微好一些,但是整体差距不大。其实,除了最基本的情况,还有一种特殊情况。还记得我们前面提到的Object中键的排序吗?我们提到首先列出非负整数。事实上,对于非负整数作为键值和其他类型作为键值,v8会区别对待它们。负整数作为key的部分会被当成一个数组,即当非负整数具有一定的连续性时,会被认为是快速数组,如果过于稀疏则会被认为作为慢速阵列。对于快速数组,它有连续的内存,所以读写的时候会更快,占用的内存也更少。更多内容请看这篇:探索JSV8引擎下“数组”的底层实现。当key为连续非负整数时,表现如下:我们可以看到Object不仅平均速度更快,而且内存消耗也很大。减少。总结通过对比,我们可以发现Map和Object各有优缺点。针对不同的情况,我们应该做出不同的选择。所以我总结了一下我认为用Map和Object比较合适的时机。UseMap:当存储的key不是字符串/数字/或Symbol时,选择Map,因为Object不支持存储大量数据,选择Map,因为占用内存少,需要很多操作来增加/删除元素选择Map的时候,因为比较快,如果需要保持插入的顺序,就选择Map,因为Object会改变顺序。如果需要迭代/遍历,选择Map,因为它默认是可迭代对象,迭代更方便。使用Object:只是对于简单的数据结构,选择Object,因为它在数据少的时候占用内存少,新建的时候效率更高。当需要使用JSON进行文件传输时,选择Object,因为JSON默认不支持Map,需要多个key计算值时,选择Object,语法更简洁。当您需要覆盖原型上的键时,请选择对象。虽然Map在很多情况下比Object更高效,但是Object始终是JS中最基本的引用类型。它的作用不仅仅是存储键值对。